@exodus/solana-api 3.29.6 → 3.30.1
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 +28 -0
- package/package.json +3 -3
- package/src/account-state.js +1 -0
- package/src/api.js +8 -2
- package/src/clarity-api.js +7 -3
- package/src/fee-payer.js +25 -12
- package/src/tx-log/clarity-monitor.js +187 -41
- package/src/tx-log/ws-monitor.js +1 -2
- package/src/ws-api.js +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
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.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.0...@exodus/solana-api@3.30.1) (2026-03-13)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix(solana-api): add RPC concurrency limit and fix historyCursor loss on error (#7550)
|
|
13
|
+
|
|
14
|
+
* fix(solana-api): improve fetchival error handling in fee payer client (#7570)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## [3.30.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.6...@exodus/solana-api@3.30.0) (2026-03-10)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
* feat: switch Solana txs to Clarity (#7449)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
* fix: helius vote false param (#7486)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
6
34
|
## [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
35
|
|
|
8
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.30.1",
|
|
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",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"url-join": "^4.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@exodus/asset": "^2.
|
|
48
|
+
"@exodus/asset": "^2.3.0",
|
|
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": "51cc10fa866ae472882aa09353e2425bc7d7031f",
|
|
53
53
|
"bugs": {
|
|
54
54
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
55
55
|
},
|
package/src/account-state.js
CHANGED
|
@@ -17,6 +17,7 @@ export const createAccountState = ({ assetList }) => {
|
|
|
17
17
|
return class SolanaAccountState extends AccountState {
|
|
18
18
|
static defaults = {
|
|
19
19
|
cursor: '',
|
|
20
|
+
historyCursor: '',
|
|
20
21
|
balance: asset.currency.ZERO,
|
|
21
22
|
tokenBalances: Object.create(null),
|
|
22
23
|
rentExemptAmount: asset.currency.ZERO,
|
package/src/api.js
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
TOKEN_PROGRAM_ID,
|
|
20
20
|
} from '@exodus/solana-lib'
|
|
21
21
|
import lodash from 'lodash'
|
|
22
|
+
import makeConcurrent from 'make-concurrent'
|
|
22
23
|
import ms from 'ms'
|
|
23
24
|
import urljoin from 'url-join'
|
|
24
25
|
|
|
@@ -41,6 +42,8 @@ const errorMessagesToRetry = [
|
|
|
41
42
|
'Failed to query long-term storage; please try again',
|
|
42
43
|
]
|
|
43
44
|
|
|
45
|
+
const TX_DETAIL_FETCH_CONCURRENCY = 10
|
|
46
|
+
|
|
44
47
|
// Tokens + SOL api support
|
|
45
48
|
export class Api {
|
|
46
49
|
constructor({ rpcUrl, wsUrl, assets, txsLimit }) {
|
|
@@ -230,8 +233,11 @@ export class Api {
|
|
|
230
233
|
let txsId = txsResultsByAccount.flat() // merge arrays
|
|
231
234
|
txsId = lodash.uniqBy(txsId, 'signature')
|
|
232
235
|
|
|
233
|
-
// get txs details
|
|
234
|
-
const
|
|
236
|
+
// get txs details with concurrency limit to avoid overwhelming the RPC server
|
|
237
|
+
const fetchWithLimit = makeConcurrent((signature) => this.getTransactionById(signature), {
|
|
238
|
+
concurrency: TX_DETAIL_FETCH_CONCURRENCY,
|
|
239
|
+
})
|
|
240
|
+
const txsDetails = await Promise.all(txsId.map((tx) => fetchWithLimit(tx.signature)))
|
|
235
241
|
txsDetails.forEach((txDetail) => {
|
|
236
242
|
if (!txDetail) return
|
|
237
243
|
|
package/src/clarity-api.js
CHANGED
|
@@ -7,7 +7,7 @@ import urljoin from 'url-join'
|
|
|
7
7
|
|
|
8
8
|
import { RpcApi } from './rpc-api.js'
|
|
9
9
|
|
|
10
|
-
const CLARITY_URL = 'https://
|
|
10
|
+
const CLARITY_URL = 'https://assets-gateway-clarity-api.a.exodus.io/assets/api/v2/solana'
|
|
11
11
|
|
|
12
12
|
// Filter out nil and empty string values to prevent API validation errors (e.g., empty cursor)
|
|
13
13
|
const cleanQuery = (obj) => omitBy(obj, (v) => isNil(v) || v === '')
|
|
@@ -47,11 +47,15 @@ export class ClarityApi extends RpcApi {
|
|
|
47
47
|
})
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async getTransactions(
|
|
50
|
+
async getTransactions(
|
|
51
|
+
address,
|
|
52
|
+
{ after, before, limit, includeUnparsed = false } = Object.create(null)
|
|
53
|
+
) {
|
|
51
54
|
const result = await this.request(`/addresses/${encodeURIComponent(address)}/transactions`)
|
|
52
55
|
.query(
|
|
53
56
|
cleanQuery({
|
|
54
|
-
|
|
57
|
+
before,
|
|
58
|
+
after,
|
|
55
59
|
limit,
|
|
56
60
|
includeUnparsed,
|
|
57
61
|
})
|
package/src/fee-payer.js
CHANGED
|
@@ -64,14 +64,30 @@ export const feePayerClientFactory = ({
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
try {
|
|
68
|
+
return await fetchival(`${feePayerApiUrl}/transactions/sponsor`, {
|
|
69
|
+
mode: 'cors',
|
|
70
|
+
cache: 'no-cache',
|
|
71
|
+
timeout: ms('10s'),
|
|
72
|
+
headers,
|
|
73
|
+
}).post({
|
|
74
|
+
transaction: encodedTransaction,
|
|
75
|
+
})
|
|
76
|
+
} catch (err) {
|
|
77
|
+
let nerr = err
|
|
78
|
+
if (err.response) {
|
|
79
|
+
try {
|
|
80
|
+
const data = await err.response.text()
|
|
81
|
+
nerr = new Error(`${err.response.status}: ${data}`)
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (err.response?.status) {
|
|
86
|
+
nerr.status = err.response.status
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw nerr
|
|
90
|
+
}
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
/**
|
|
@@ -84,10 +100,7 @@ export const feePayerClientFactory = ({
|
|
|
84
100
|
try {
|
|
85
101
|
response = await makeSponsorRequest({ encodedTransaction })
|
|
86
102
|
} catch (error) {
|
|
87
|
-
if (
|
|
88
|
-
requireAuthentication &&
|
|
89
|
-
(error.response?.status === 401 || error.response?.status === 403)
|
|
90
|
-
) {
|
|
103
|
+
if (requireAuthentication && (error.status === 401 || error.status === 403)) {
|
|
91
104
|
console.warn('Authentication failed, retrying...')
|
|
92
105
|
if (authClient) {
|
|
93
106
|
await authClient._authenticate()
|
|
@@ -18,21 +18,21 @@ const TICKS_BETWEEN_STAKE_FETCHES = 5
|
|
|
18
18
|
const TX_STALE_AFTER = ms('2m') // mark txs as dropped after N minutes
|
|
19
19
|
|
|
20
20
|
export class SolanaClarityMonitor extends BaseMonitor {
|
|
21
|
+
#maxFetchLimitPerTick
|
|
22
|
+
|
|
21
23
|
constructor({
|
|
22
24
|
clarityApi,
|
|
23
|
-
rpcApi,
|
|
24
25
|
includeUnparsed = false,
|
|
25
26
|
ticksBetweenHistoryFetches = TICKS_BETWEEN_HISTORY_FETCHES,
|
|
26
27
|
ticksBetweenStakeFetches = TICKS_BETWEEN_STAKE_FETCHES,
|
|
27
28
|
txsLimit,
|
|
29
|
+
maxFetchLimitPerTick = 5,
|
|
28
30
|
shouldUpdateBalanceBeforeHistory = true,
|
|
29
31
|
...args
|
|
30
32
|
}) {
|
|
31
33
|
super(args)
|
|
32
34
|
assert(clarityApi, 'clarityApi is required')
|
|
33
35
|
this.clarityApi = clarityApi
|
|
34
|
-
this.rpcApi = rpcApi
|
|
35
|
-
this.cursors = Object.create(null)
|
|
36
36
|
this.assets = Object.create(null)
|
|
37
37
|
this.staking = DEFAULT_REMOTE_CONFIG.staking
|
|
38
38
|
this.ticksBetweenStakeFetches = ticksBetweenStakeFetches
|
|
@@ -40,6 +40,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
40
40
|
this.shouldUpdateBalanceBeforeHistory = shouldUpdateBalanceBeforeHistory
|
|
41
41
|
this.includeUnparsed = includeUnparsed
|
|
42
42
|
this.txsLimit = txsLimit
|
|
43
|
+
this.#maxFetchLimitPerTick = maxFetchLimitPerTick
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
setServer(config = Object.create(null)) {
|
|
@@ -53,11 +54,6 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
53
54
|
this.staking = staking
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
hasNewCursor({ walletAccount, cursorState }) {
|
|
57
|
-
const { cursor } = cursorState
|
|
58
|
-
return this.cursors[walletAccount] !== cursor
|
|
59
|
-
}
|
|
60
|
-
|
|
61
57
|
async emitUnknownTokensEvent({ tokenAccounts }) {
|
|
62
58
|
const tokensList = await this.clarityApi.getWalletTokensList({ tokenAccounts })
|
|
63
59
|
const unknownTokensList = tokensList.filter((mintAddress) => {
|
|
@@ -101,7 +97,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
101
97
|
const txSet = await this.aci.getTxLog({ assetName, walletAccount })
|
|
102
98
|
const { stale } = this.getUnconfirmed({ txSet, staleTxAge: TX_STALE_AFTER })
|
|
103
99
|
if (stale.length > 0) {
|
|
104
|
-
clearedLogItems[assetName] = lodash.unionBy(logItemsByAsset[assetName], stale, 'txId')
|
|
100
|
+
clearedLogItems[assetName] = lodash.unionBy(logItemsByAsset[assetName] ?? [], stale, 'txId')
|
|
105
101
|
}
|
|
106
102
|
}
|
|
107
103
|
|
|
@@ -137,9 +133,9 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
137
133
|
})
|
|
138
134
|
const hasUnconfirmedSentTx = [...baseAssetTxLog].some((tx) => tx.pending && tx.sent)
|
|
139
135
|
|
|
140
|
-
const
|
|
136
|
+
const shouldUpdateLatestHistory =
|
|
141
137
|
refresh || isHistoryUpdateTick || balanceChanged || hasUnconfirmedSentTx
|
|
142
|
-
const shouldUpdateOnlyBalance = balanceChanged && !
|
|
138
|
+
const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateLatestHistory
|
|
143
139
|
|
|
144
140
|
// start a batch
|
|
145
141
|
const batch = this.aci.createOperationsBatch()
|
|
@@ -150,50 +146,147 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
150
146
|
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
151
147
|
}
|
|
152
148
|
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
const newCursorState = {
|
|
150
|
+
cursor: accountState.cursor,
|
|
151
|
+
historyCursor: accountState.historyCursor,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const newTransactions = []
|
|
155
|
+
|
|
156
|
+
if (shouldUpdateLatestHistory) {
|
|
157
|
+
const { transactions: latestTransactions, cursorState: latestHistoryCursorState } =
|
|
158
|
+
await this.getLatestHistory({
|
|
159
|
+
address,
|
|
160
|
+
accountState,
|
|
161
|
+
walletAccount,
|
|
162
|
+
refresh,
|
|
163
|
+
tokenAccounts,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
if (latestHistoryCursorState.cursor) {
|
|
167
|
+
newCursorState.cursor = latestHistoryCursorState.cursor
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (latestHistoryCursorState.historyCursor) {
|
|
171
|
+
// refresh case will have historyCursor available
|
|
172
|
+
// but if user has 0 txs, it won't be available, therefore need to check
|
|
173
|
+
newCursorState.historyCursor = latestHistoryCursorState.historyCursor
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
newTransactions.push(...latestTransactions)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const shouldFetchOldHistory = !refresh && newCursorState.historyCursor // on refresh request, wait until next tick to fetch old history
|
|
180
|
+
if (shouldFetchOldHistory) {
|
|
181
|
+
const { transactions: historyTransactions, historyCursor } = await this.fetchOldHistory({
|
|
155
182
|
address,
|
|
156
|
-
|
|
157
|
-
walletAccount,
|
|
158
|
-
refresh,
|
|
159
|
-
tokenAccounts,
|
|
183
|
+
historyCursor: newCursorState.historyCursor,
|
|
160
184
|
})
|
|
161
185
|
|
|
162
|
-
|
|
186
|
+
newTransactions.push(...historyTransactions)
|
|
187
|
+
// Always update so we persist historyCursor = undefined when history is exhausted
|
|
188
|
+
newCursorState.historyCursor = historyCursor
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
163
192
|
|
|
193
|
+
if (newTransactions.length > 0) {
|
|
164
194
|
// update all state at once
|
|
165
|
-
const clearedLogItems = await this.markStaleTransactions({
|
|
195
|
+
const clearedLogItems = await this.markStaleTransactions({
|
|
196
|
+
walletAccount,
|
|
197
|
+
logItemsByAsset: this.mapTransactionsToLogItems({ transactions: newTransactions, address }),
|
|
198
|
+
})
|
|
199
|
+
|
|
166
200
|
this.updateTxLogByAssetBatch({
|
|
167
201
|
logItemsByAsset: clearedLogItems,
|
|
168
202
|
walletAccount,
|
|
169
203
|
refresh,
|
|
170
204
|
batch,
|
|
171
205
|
})
|
|
172
|
-
this.updateState({ account, cursorState, walletAccount, staking, batch })
|
|
173
|
-
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
174
|
-
if (refresh || cursorChanged) {
|
|
175
|
-
this.cursors[walletAccount] = cursorState.cursor
|
|
176
|
-
}
|
|
177
206
|
}
|
|
178
207
|
|
|
208
|
+
// Always persist cursor state so historyCursor advances (and is cleared when history exhausted)
|
|
209
|
+
this.updateState({
|
|
210
|
+
account,
|
|
211
|
+
cursorState: newCursorState,
|
|
212
|
+
walletAccount,
|
|
213
|
+
staking,
|
|
214
|
+
batch,
|
|
215
|
+
})
|
|
216
|
+
|
|
179
217
|
// close batch
|
|
180
218
|
await this.aci.executeOperationsBatch(batch)
|
|
181
219
|
}
|
|
182
220
|
|
|
183
|
-
async
|
|
184
|
-
|
|
185
|
-
const baseAsset = this.asset
|
|
221
|
+
async fetchOldHistory({ address, historyCursor }) {
|
|
222
|
+
if (!historyCursor) return { transactions: [], historyCursor: undefined }
|
|
186
223
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
224
|
+
try {
|
|
225
|
+
const { transactions, before } = await this.clarityApi.getTransactions(address, {
|
|
226
|
+
before: historyCursor,
|
|
227
|
+
includeUnparsed: this.includeUnparsed,
|
|
228
|
+
limit: this.txsLimit,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
if (!transactions || transactions.length === 0 || !before) {
|
|
232
|
+
// no more transactions to fetch, return undefined to stop fetching old history
|
|
233
|
+
return { transactions: [], historyCursor: undefined }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const transactionsWithSilentSound = transactions.map((tx) => ({
|
|
237
|
+
...tx,
|
|
238
|
+
data: { ...tx.data, silentSound: true },
|
|
239
|
+
}))
|
|
240
|
+
|
|
241
|
+
return { transactions: transactionsWithSilentSound, historyCursor: before }
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.warn('SolanaClarityMonitor fetchOldHistory failed', {
|
|
244
|
+
address,
|
|
245
|
+
historyCursor,
|
|
246
|
+
error,
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// Preserve cursor on transient errors so fetching retries on the next tick
|
|
250
|
+
return { transactions: [], historyCursor }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async getLatestHistory({ address, accountState, refresh, tokenAccounts } = Object.create(null)) {
|
|
255
|
+
if (refresh || !accountState.cursor) {
|
|
256
|
+
const { transactions, before, after } = await this.clarityApi.getTransactions(address, {
|
|
257
|
+
after: undefined,
|
|
258
|
+
includeUnparsed: this.includeUnparsed,
|
|
259
|
+
limit: this.txsLimit,
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
transactions,
|
|
264
|
+
hasNewTxs: transactions.length > 0,
|
|
265
|
+
cursorState: {
|
|
266
|
+
cursor: after,
|
|
267
|
+
historyCursor: before,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const { transactions, after: newAfterCursor } = await this.fetchLatestTransactions({
|
|
273
|
+
address,
|
|
274
|
+
afterCursor: accountState.cursor,
|
|
194
275
|
})
|
|
195
276
|
|
|
277
|
+
return {
|
|
278
|
+
transactions,
|
|
279
|
+
hasNewTxs: transactions.length > 0,
|
|
280
|
+
cursorState: {
|
|
281
|
+
cursor: newAfterCursor,
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
mapTransactionsToLogItems({ transactions, address }) {
|
|
287
|
+
const baseAsset = this.asset
|
|
196
288
|
const mappedTransactions = []
|
|
289
|
+
|
|
197
290
|
for (const tx of transactions) {
|
|
198
291
|
// we get the token name using the token.mintAddress
|
|
199
292
|
const assetName = tx.token
|
|
@@ -218,6 +311,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
218
311
|
staking: tx.staking || null,
|
|
219
312
|
unparsed: !!tx.unparsed,
|
|
220
313
|
swapTx: !!(tx.data && tx.data.inner),
|
|
314
|
+
silentSound: !!tx.data?.silentSound,
|
|
221
315
|
},
|
|
222
316
|
currencies: { [assetName]: asset.currency, [feeAsset.name]: feeAsset.currency },
|
|
223
317
|
}
|
|
@@ -262,12 +356,63 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
262
356
|
mappedTransactions.push(item)
|
|
263
357
|
}
|
|
264
358
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
359
|
+
return lodash.groupBy(mergeByTxId(mappedTransactions), 'coinName')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async fetchLatestTransactions({ address, afterCursor }) {
|
|
363
|
+
let cursor = afterCursor
|
|
364
|
+
let allTransactions = []
|
|
365
|
+
let newAfterCursor = afterCursor
|
|
366
|
+
let fetchCount = 0
|
|
367
|
+
|
|
368
|
+
while (fetchCount < this.#maxFetchLimitPerTick) {
|
|
369
|
+
try {
|
|
370
|
+
const { transactions, after } = await this.clarityApi.getTransactions(address, {
|
|
371
|
+
after: cursor,
|
|
372
|
+
includeUnparsed: this.includeUnparsed,
|
|
373
|
+
limit: this.txsLimit,
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
fetchCount += 1
|
|
377
|
+
|
|
378
|
+
if (!transactions || transactions.length === 0) {
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!after) {
|
|
383
|
+
// guard against missing cursor on non-empty transactions
|
|
384
|
+
console.warn('SolanaClarityMonitor missing cursor with transactions', {
|
|
385
|
+
address,
|
|
386
|
+
cursor,
|
|
387
|
+
afterCursor,
|
|
388
|
+
fetchCount,
|
|
389
|
+
transactionsCount: transactions.length,
|
|
390
|
+
})
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
allTransactions = [...allTransactions, ...transactions]
|
|
395
|
+
|
|
396
|
+
if (after === cursor) {
|
|
397
|
+
newAfterCursor = after
|
|
398
|
+
break
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
cursor = after
|
|
402
|
+
newAfterCursor = after
|
|
403
|
+
} catch (error) {
|
|
404
|
+
console.warn('SolanaClarityMonitor fetchLatestTransactions failed', {
|
|
405
|
+
address,
|
|
406
|
+
cursor,
|
|
407
|
+
afterCursor,
|
|
408
|
+
fetchCount,
|
|
409
|
+
error,
|
|
410
|
+
})
|
|
411
|
+
break
|
|
412
|
+
}
|
|
270
413
|
}
|
|
414
|
+
|
|
415
|
+
return { transactions: allTransactions, after: newAfterCursor }
|
|
271
416
|
}
|
|
272
417
|
|
|
273
418
|
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
@@ -367,17 +512,18 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
367
512
|
stakingInfo: staking,
|
|
368
513
|
...cursorState,
|
|
369
514
|
}
|
|
515
|
+
|
|
370
516
|
return this.updateAccountStateBatch({ assetName, walletAccount, newData, batch })
|
|
371
517
|
}
|
|
372
518
|
|
|
373
519
|
async getStakingInfo({ address, accountState, walletAccount }) {
|
|
374
520
|
const stakingInfo = await this.clarityApi.getStakeAccountsInfo(address)
|
|
375
|
-
let earned = accountState.stakingInfo
|
|
521
|
+
let earned = accountState.stakingInfo?.earned?.toBaseString() ?? '0'
|
|
376
522
|
try {
|
|
377
523
|
const rewards = await this.clarityApi.getRewards(address)
|
|
378
524
|
earned = rewards
|
|
379
525
|
} catch (error) {
|
|
380
|
-
console.warn(error)
|
|
526
|
+
console.warn('SolanaClarityMonitor getStakingInfo failed', { address, error })
|
|
381
527
|
}
|
|
382
528
|
|
|
383
529
|
return {
|
package/src/tx-log/ws-monitor.js
CHANGED
|
@@ -26,7 +26,6 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
26
26
|
async beforeStart() {
|
|
27
27
|
this.assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.asset.name })
|
|
28
28
|
this.clarityApi.setTokens(this.assets)
|
|
29
|
-
this.rpcApi.setTokens(this.assets)
|
|
30
29
|
|
|
31
30
|
const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.asset.name })
|
|
32
31
|
await Promise.all(
|
|
@@ -50,7 +49,7 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
50
49
|
useCache: true,
|
|
51
50
|
})
|
|
52
51
|
|
|
53
|
-
const { accounts: tokenAccountsByOwner } = await this.
|
|
52
|
+
const { accounts: tokenAccountsByOwner } = await this.clarityApi.getTokensBalancesAndAccounts({
|
|
54
53
|
address,
|
|
55
54
|
})
|
|
56
55
|
this.tokenAccountsByOwner[walletAccount] = tokenAccountsByOwner
|
package/src/ws-api.js
CHANGED
|
@@ -7,7 +7,7 @@ import { parseTransaction } from './tx-parser.js'
|
|
|
7
7
|
import { isSolAddressPoisoningTx } from './txs-utils.js'
|
|
8
8
|
|
|
9
9
|
// Triton Whirligig WebSocket
|
|
10
|
-
const TRITON_WS_ENDPOINT = 'wss://solana-triton.a.exodus.io/whirligig' // pointing to: wss://exodus-solanama-6db3.mainnet.rpcpool.com/<token>/whirligig
|
|
10
|
+
// const TRITON_WS_ENDPOINT = 'wss://solana-triton.a.exodus.io/whirligig' // pointing to: wss://exodus-solanama-6db3.mainnet.rpcpool.com/<token>/whirligig
|
|
11
11
|
// Helius Enhanced WebSocket (https://www.helius.dev/docs/enhanced-websockets)
|
|
12
12
|
const HELIUS_WS_URL = 'wss://solana-helius-wss.a.exodus.io/ws'
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ export class WsApi {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
setWsEndpoint(wsUrl) {
|
|
34
|
-
this.wsUrl = wsUrl ||
|
|
34
|
+
this.wsUrl = wsUrl || HELIUS_WS_URL
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** True when using Helius Enhanced WebSocket (different transactionSubscribe params and notification shape). */
|
|
@@ -189,7 +189,7 @@ export class WsApi {
|
|
|
189
189
|
maxSupportedTransactionVersion: 255,
|
|
190
190
|
}
|
|
191
191
|
const filter = this.#isHelius()
|
|
192
|
-
? { accountInclude: difference }
|
|
192
|
+
? { vote: false, accountInclude: difference }
|
|
193
193
|
: { vote: false, accounts: { include: difference } }
|
|
194
194
|
conn.send({
|
|
195
195
|
jsonrpc: '2.0',
|