@exodus/solana-api 3.30.2 → 3.30.3
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/api.js +32 -4
- package/src/tx-log/clarity-monitor.js +156 -102
- package/src/tx-log/ws-monitor.js +28 -9
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.30.3](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.2...@exodus/solana-api@3.30.3) (2026-03-20)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: solana zero balance on restore under transient Clarity/RPC failures (#7628)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
6
16
|
## [3.30.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.30.1...@exodus/solana-api@3.30.2) (2026-03-18)
|
|
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.30.
|
|
3
|
+
"version": "3.30.3",
|
|
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": "f1f7c3ad9d55f774c9c6a09434139d46f605e43c",
|
|
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
|
@@ -215,13 +215,23 @@ export class Api {
|
|
|
215
215
|
try {
|
|
216
216
|
const until = cursor
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
let tokenAccountsByOwner
|
|
219
|
+
try {
|
|
220
|
+
tokenAccountsByOwner = tokenAccounts || (await this.getTokenAccountsByOwner(address))
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.warn('Solana getTokenAccountsByOwner failed, continuing with main address only', {
|
|
223
|
+
address,
|
|
224
|
+
error: err,
|
|
225
|
+
})
|
|
226
|
+
tokenAccountsByOwner = []
|
|
227
|
+
}
|
|
228
|
+
|
|
219
229
|
const tokenAccountAddresses = tokenAccountsByOwner
|
|
220
230
|
.filter(({ tokenName }) => tokenName !== 'unknown')
|
|
221
231
|
.map(({ tokenAccountAddress }) => tokenAccountAddress)
|
|
222
232
|
const accountsToCheck = [address, ...tokenAccountAddresses]
|
|
223
233
|
|
|
224
|
-
const
|
|
234
|
+
const signatureSettled = await Promise.allSettled(
|
|
225
235
|
accountsToCheck.map((addr) =>
|
|
226
236
|
this.getSignaturesForAddress(addr, {
|
|
227
237
|
until,
|
|
@@ -230,14 +240,32 @@ export class Api {
|
|
|
230
240
|
})
|
|
231
241
|
)
|
|
232
242
|
)
|
|
243
|
+
const txsResultsByAccount = signatureSettled.map((result, index) => {
|
|
244
|
+
if (result.status === 'fulfilled') return result.value
|
|
245
|
+
console.warn('Solana getSignaturesForAddress failed', {
|
|
246
|
+
address: accountsToCheck[index],
|
|
247
|
+
error: result.reason,
|
|
248
|
+
})
|
|
249
|
+
return []
|
|
250
|
+
})
|
|
233
251
|
let txsId = txsResultsByAccount.flat() // merge arrays
|
|
234
252
|
txsId = lodash.uniqBy(txsId, 'signature')
|
|
235
253
|
|
|
236
|
-
//
|
|
254
|
+
// Per-tx fetch: one failure must not drop the whole page. Concurrency still capped.
|
|
237
255
|
const fetchWithLimit = makeConcurrent((signature) => this.getTransactionById(signature), {
|
|
238
256
|
concurrency: TX_DETAIL_FETCH_CONCURRENCY,
|
|
239
257
|
})
|
|
240
|
-
const txsDetails = await Promise.all(
|
|
258
|
+
const txsDetails = await Promise.all(
|
|
259
|
+
txsId.map((tx) =>
|
|
260
|
+
fetchWithLimit(tx.signature).catch((error) => {
|
|
261
|
+
console.warn('Solana getTransactionById failed', {
|
|
262
|
+
signature: tx.signature,
|
|
263
|
+
error,
|
|
264
|
+
})
|
|
265
|
+
return null
|
|
266
|
+
})
|
|
267
|
+
)
|
|
268
|
+
)
|
|
241
269
|
txsDetails.forEach((txDetail) => {
|
|
242
270
|
if (!txDetail) return
|
|
243
271
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
2
|
import { mapValues, pickBy } from '@exodus/basic-utils'
|
|
3
|
+
import { retry } from '@exodus/simple-retry'
|
|
3
4
|
import lodash from 'lodash'
|
|
4
5
|
import assert from 'minimalistic-assert'
|
|
5
6
|
import ms from 'ms'
|
|
@@ -88,6 +89,33 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
88
89
|
)
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
#normalizeAccountInfoPayload(accountInfo) {
|
|
93
|
+
if (!accountInfo) return accountInfo
|
|
94
|
+
const inner = accountInfo.value
|
|
95
|
+
if (!inner || typeof inner !== 'object') return accountInfo
|
|
96
|
+
return {
|
|
97
|
+
...accountInfo,
|
|
98
|
+
lamports: accountInfo.lamports ?? inner.lamports,
|
|
99
|
+
space: accountInfo.space ?? inner.space,
|
|
100
|
+
owner: accountInfo.owner ?? inner.owner,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async #fetchAccountInfoWithRetry(address) {
|
|
105
|
+
const fetchRetry = retry(() => this.clarityApi.getAccountInfo(address), {
|
|
106
|
+
delayTimesMs: ['200ms', '400ms', '800ms'],
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
return await fetchRetry()
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn('SolanaClarityMonitor getAccountInfo failed after retries', {
|
|
113
|
+
address,
|
|
114
|
+
error,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
91
119
|
async markStaleTransactions({ walletAccount, logItemsByAsset = Object.create(null) }) {
|
|
92
120
|
// mark stale txs as dropped in logItemsByAsset
|
|
93
121
|
const clearedLogItems = logItemsByAsset
|
|
@@ -137,85 +165,81 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
137
165
|
refresh || isHistoryUpdateTick || balanceChanged || hasUnconfirmedSentTx
|
|
138
166
|
const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateLatestHistory
|
|
139
167
|
|
|
140
|
-
// start a batch
|
|
141
168
|
const batch = this.aci.createOperationsBatch()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
169
|
+
try {
|
|
170
|
+
if (this.shouldUpdateBalanceBeforeHistory || shouldUpdateOnlyBalance) {
|
|
171
|
+
this.updateState({ account, walletAccount, staking, batch })
|
|
172
|
+
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
173
|
+
}
|
|
148
174
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
175
|
+
const newCursorState = {
|
|
176
|
+
cursor: accountState.cursor,
|
|
177
|
+
historyCursor: accountState.historyCursor,
|
|
178
|
+
}
|
|
153
179
|
|
|
154
|
-
|
|
180
|
+
const newTransactions = []
|
|
155
181
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
182
|
+
if (shouldUpdateLatestHistory) {
|
|
183
|
+
const { transactions: latestTransactions, cursorState: latestHistoryCursorState } =
|
|
184
|
+
await this.getLatestHistory({
|
|
185
|
+
address,
|
|
186
|
+
accountState,
|
|
187
|
+
walletAccount,
|
|
188
|
+
refresh,
|
|
189
|
+
tokenAccounts,
|
|
190
|
+
})
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
192
|
+
if (latestHistoryCursorState.cursor) {
|
|
193
|
+
newCursorState.cursor = latestHistoryCursorState.cursor
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (latestHistoryCursorState.historyCursor) {
|
|
197
|
+
newCursorState.historyCursor = latestHistoryCursorState.historyCursor
|
|
198
|
+
}
|
|
169
199
|
|
|
170
|
-
|
|
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
|
|
200
|
+
newTransactions.push(...latestTransactions)
|
|
174
201
|
}
|
|
175
202
|
|
|
176
|
-
|
|
177
|
-
|
|
203
|
+
const shouldFetchOldHistory = !refresh && newCursorState.historyCursor // on refresh request, wait until next tick to fetch old history
|
|
204
|
+
if (shouldFetchOldHistory) {
|
|
205
|
+
const { transactions: historyTransactions, historyCursor } = await this.fetchOldHistory({
|
|
206
|
+
address,
|
|
207
|
+
historyCursor: newCursorState.historyCursor,
|
|
208
|
+
})
|
|
178
209
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
address,
|
|
183
|
-
historyCursor: newCursorState.historyCursor,
|
|
184
|
-
})
|
|
210
|
+
newTransactions.push(...historyTransactions)
|
|
211
|
+
newCursorState.historyCursor = historyCursor
|
|
212
|
+
}
|
|
185
213
|
|
|
186
|
-
|
|
187
|
-
// Always update so we persist historyCursor = undefined when history is exhausted
|
|
188
|
-
newCursorState.historyCursor = historyCursor
|
|
189
|
-
}
|
|
214
|
+
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
190
215
|
|
|
191
|
-
|
|
216
|
+
if (newTransactions.length > 0) {
|
|
217
|
+
const clearedLogItems = await this.markStaleTransactions({
|
|
218
|
+
walletAccount,
|
|
219
|
+
logItemsByAsset: this.mapTransactionsToLogItems({
|
|
220
|
+
transactions: newTransactions,
|
|
221
|
+
address,
|
|
222
|
+
}),
|
|
223
|
+
})
|
|
192
224
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
225
|
+
this.updateTxLogByAssetBatch({
|
|
226
|
+
logItemsByAsset: clearedLogItems,
|
|
227
|
+
walletAccount,
|
|
228
|
+
refresh,
|
|
229
|
+
batch,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
199
232
|
|
|
200
|
-
this.
|
|
201
|
-
|
|
233
|
+
this.updateState({
|
|
234
|
+
account,
|
|
235
|
+
cursorState: newCursorState,
|
|
202
236
|
walletAccount,
|
|
203
|
-
|
|
237
|
+
staking,
|
|
204
238
|
batch,
|
|
205
239
|
})
|
|
240
|
+
} finally {
|
|
241
|
+
await this.aci.executeOperationsBatch(batch)
|
|
206
242
|
}
|
|
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
|
-
|
|
217
|
-
// close batch
|
|
218
|
-
await this.aci.executeOperationsBatch(batch)
|
|
219
243
|
}
|
|
220
244
|
|
|
221
245
|
async fetchOldHistory({ address, historyCursor }) {
|
|
@@ -229,7 +253,6 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
229
253
|
})
|
|
230
254
|
|
|
231
255
|
if (!transactions || transactions.length === 0 || !before) {
|
|
232
|
-
// no more transactions to fetch, return undefined to stop fetching old history
|
|
233
256
|
return { transactions: [], historyCursor: undefined }
|
|
234
257
|
}
|
|
235
258
|
|
|
@@ -288,7 +311,6 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
288
311
|
const mappedTransactions = []
|
|
289
312
|
|
|
290
313
|
for (const tx of transactions) {
|
|
291
|
-
// we get the token name using the token.mintAddress
|
|
292
314
|
const assetName = tx.token
|
|
293
315
|
? this.clarityApi.tokens.get(tx.token.mintAddress)?.name ?? 'unknown'
|
|
294
316
|
: baseAsset.name
|
|
@@ -304,7 +326,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
304
326
|
txId: tx.id,
|
|
305
327
|
from: [tx.from],
|
|
306
328
|
coinAmount,
|
|
307
|
-
confirmations: 1,
|
|
329
|
+
confirmations: 1,
|
|
308
330
|
date: tx.date,
|
|
309
331
|
error: tx.error,
|
|
310
332
|
data: {
|
|
@@ -317,7 +339,6 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
317
339
|
}
|
|
318
340
|
|
|
319
341
|
if (tx.owner === address) {
|
|
320
|
-
// send transaction
|
|
321
342
|
item.to = Array.isArray(tx.to) ? undefined : tx.to
|
|
322
343
|
item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
|
|
323
344
|
item.feeCoinName = baseAsset.name
|
|
@@ -380,7 +401,6 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
380
401
|
}
|
|
381
402
|
|
|
382
403
|
if (!after) {
|
|
383
|
-
// guard against missing cursor on non-empty transactions
|
|
384
404
|
console.warn('SolanaClarityMonitor missing cursor with transactions', {
|
|
385
405
|
address,
|
|
386
406
|
cursor,
|
|
@@ -417,60 +437,94 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
417
437
|
|
|
418
438
|
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
419
439
|
const delegatedAccounts = accountState.tokenDelegationInfo?.delegatedAccounts || []
|
|
420
|
-
const [
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
440
|
+
const [accountInfoRaw, tokensPayload, delegatedBalances] = await Promise.all([
|
|
441
|
+
this.#fetchAccountInfoWithRetry(address),
|
|
442
|
+
this.clarityApi.getTokensBalancesAndAccounts({ address }).catch((error) => {
|
|
443
|
+
console.warn('SolanaClarityMonitor getTokensBalancesAndAccounts failed', { address, error })
|
|
444
|
+
return null
|
|
445
|
+
}),
|
|
446
|
+
this.clarityApi.fetchDelegatedBalances({ delegatedAccounts, address }).catch((error) => {
|
|
447
|
+
console.warn('SolanaClarityMonitor fetchDelegatedBalances failed', { address, error })
|
|
448
|
+
return accountState.tokenDelegationInfo?.delegatedBalances ?? Object.create(null)
|
|
449
|
+
}),
|
|
450
|
+
])
|
|
451
|
+
|
|
452
|
+
const accountInfo = this.#normalizeAccountInfoPayload(accountInfoRaw)
|
|
453
|
+
const hasAccountInfo = accountInfo != null
|
|
454
|
+
const accountSize = hasAccountInfo ? accountInfo.space ?? 0 : accountState.accountSize ?? 0
|
|
455
|
+
const baseBalanceWithoutStaking = hasAccountInfo
|
|
456
|
+
? this.asset.currency.baseUnit(accountInfo.lamports ?? 0)
|
|
457
|
+
: accountState.balance ?? this.asset.currency.ZERO
|
|
458
|
+
|
|
459
|
+
let rentExemptAmount = accountState.rentExemptAmount ?? this.asset.currency.ZERO
|
|
460
|
+
if (hasAccountInfo) {
|
|
461
|
+
try {
|
|
462
|
+
rentExemptAmount = this.asset.currency.baseUnit(
|
|
463
|
+
await this.clarityApi.getMinimumBalanceForRentExemption(accountSize)
|
|
464
|
+
)
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.warn('SolanaClarityMonitor getMinimumBalanceForRentExemption failed', {
|
|
428
467
|
address,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const accountSize = accountInfo?.space || 0
|
|
468
|
+
accountSize,
|
|
469
|
+
error,
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
}
|
|
435
473
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
474
|
+
let ownerChanged = accountState.ownerChanged ?? false
|
|
475
|
+
if (hasAccountInfo) {
|
|
476
|
+
try {
|
|
477
|
+
ownerChanged = await this.clarityApi.ownerChanged(address, accountInfo)
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.warn('SolanaClarityMonitor ownerChanged failed', { address, error })
|
|
480
|
+
}
|
|
481
|
+
}
|
|
439
482
|
|
|
440
|
-
const
|
|
483
|
+
const splBalances =
|
|
484
|
+
tokensPayload?.balances && typeof tokensPayload.balances === 'object'
|
|
485
|
+
? tokensPayload.balances
|
|
486
|
+
: Object.create(null)
|
|
487
|
+
const tokenAccounts = Array.isArray(tokensPayload?.accounts) ? tokensPayload.accounts : []
|
|
441
488
|
|
|
442
|
-
const tokenBalances =
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
489
|
+
const tokenBalances = tokensPayload
|
|
490
|
+
? mapValues(
|
|
491
|
+
pickBy(splBalances, (_balance, name) => this.assets[name]), // filter unknown tokens
|
|
492
|
+
(balance, name) => this.assets[name].currency.baseUnit(balance)
|
|
493
|
+
)
|
|
494
|
+
: accountState.tokenBalances ?? Object.create(null)
|
|
446
495
|
|
|
447
496
|
const solBalanceChanged = this.#balanceChanged({
|
|
448
497
|
account: accountState,
|
|
449
498
|
newAccount: {
|
|
450
|
-
balance:
|
|
499
|
+
balance: baseBalanceWithoutStaking,
|
|
451
500
|
tokenBalances,
|
|
452
501
|
},
|
|
453
502
|
})
|
|
454
503
|
const fetchStakingInfo =
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
504
|
+
hasAccountInfo &&
|
|
505
|
+
(refresh ||
|
|
506
|
+
solBalanceChanged ||
|
|
507
|
+
this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0)
|
|
458
508
|
|
|
459
509
|
const staking =
|
|
460
510
|
this.isStakingEnabled() && fetchStakingInfo
|
|
461
|
-
? await this.getStakingInfo({ address, accountState, walletAccount })
|
|
511
|
+
? await this.getStakingInfo({ address, accountState, walletAccount }).catch((error) => {
|
|
512
|
+
console.warn('SolanaClarityMonitor getStakingInfo failed', { address, error })
|
|
513
|
+
return { ...accountState.stakingInfo, staking: this.staking }
|
|
514
|
+
})
|
|
462
515
|
: { ...accountState.stakingInfo, staking: this.staking }
|
|
463
516
|
|
|
464
517
|
const stakedBalance = this.asset.currency.baseUnit(staking.locked)
|
|
465
518
|
const activatingBalance = this.asset.currency.baseUnit(staking.activating)
|
|
466
519
|
const withdrawableBalance = this.asset.currency.baseUnit(staking.withdrawable)
|
|
467
520
|
const pendingBalance = this.asset.currency.baseUnit(staking.pending)
|
|
468
|
-
const balance =
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
521
|
+
const balance = hasAccountInfo
|
|
522
|
+
? baseBalanceWithoutStaking
|
|
523
|
+
.add(stakedBalance)
|
|
524
|
+
.add(activatingBalance)
|
|
525
|
+
.add(withdrawableBalance)
|
|
526
|
+
.add(pendingBalance)
|
|
527
|
+
: baseBalanceWithoutStaking
|
|
474
528
|
|
|
475
529
|
return {
|
|
476
530
|
account: {
|
|
@@ -535,9 +589,9 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
535
589
|
locked: this.asset.currency.baseUnit(stakingInfo.locked),
|
|
536
590
|
activating: this.asset.currency.baseUnit(stakingInfo.activating),
|
|
537
591
|
withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable),
|
|
538
|
-
pending: this.asset.currency.baseUnit(stakingInfo.pending),
|
|
592
|
+
pending: this.asset.currency.baseUnit(stakingInfo.pending),
|
|
539
593
|
earned: this.asset.currency.baseUnit(earned),
|
|
540
|
-
accounts: stakingInfo.accounts,
|
|
594
|
+
accounts: stakingInfo.accounts,
|
|
541
595
|
}
|
|
542
596
|
}
|
|
543
597
|
}
|
package/src/tx-log/ws-monitor.js
CHANGED
|
@@ -12,6 +12,16 @@ const DEFAULT_REMOTE_CONFIG = {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
15
|
+
#clarityInitialSyncDone = Object.create(null)
|
|
16
|
+
|
|
17
|
+
async #hasLoadedStakingInfo(walletAccount) {
|
|
18
|
+
const accountState = await this.aci.getAccountState({
|
|
19
|
+
assetName: this.asset.name,
|
|
20
|
+
walletAccount,
|
|
21
|
+
})
|
|
22
|
+
return accountState.stakingInfo?.loaded === true
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
constructor({ wsApi, ...args }) {
|
|
16
26
|
assert(wsApi, 'wsApi is required')
|
|
17
27
|
super(args)
|
|
@@ -49,9 +59,16 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
49
59
|
useCache: true,
|
|
50
60
|
})
|
|
51
61
|
|
|
52
|
-
const
|
|
53
|
-
address
|
|
54
|
-
|
|
62
|
+
const tokenAccountsByOwner = await this.clarityApi
|
|
63
|
+
.getTokensBalancesAndAccounts({ address })
|
|
64
|
+
.then((result) => result.accounts ?? [])
|
|
65
|
+
.catch((error) => {
|
|
66
|
+
console.warn('SolanaWebsocketMonitor getTokensBalancesAndAccounts failed', {
|
|
67
|
+
address,
|
|
68
|
+
error,
|
|
69
|
+
})
|
|
70
|
+
return []
|
|
71
|
+
})
|
|
55
72
|
this.tokenAccountsByOwner[walletAccount] = tokenAccountsByOwner
|
|
56
73
|
const tokensAddresses = tokenAccountsByOwner.map((acc) => acc.tokenAccountAddress)
|
|
57
74
|
|
|
@@ -81,10 +98,14 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
81
98
|
}
|
|
82
99
|
|
|
83
100
|
async tick({ walletAccount, refresh }) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
const shouldRunClarityTick = refresh || !this.#clarityInitialSyncDone[walletAccount]
|
|
102
|
+
if (!shouldRunClarityTick) return
|
|
103
|
+
|
|
104
|
+
// BaseMonitor increments tickCount even when tick throws.
|
|
105
|
+
// Keep retrying full sync until staking info is actually loaded.
|
|
106
|
+
await super.tick({ walletAccount, refresh })
|
|
107
|
+
if (await this.#hasLoadedStakingInfo(walletAccount)) {
|
|
108
|
+
this.#clarityInitialSyncDone[walletAccount] = true
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
@@ -181,7 +202,6 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
const newData = {
|
|
184
|
-
// ...accountState, // we don't wanna update SOL "balance"
|
|
185
205
|
tokenBalances: {
|
|
186
206
|
...accountState.tokenBalances,
|
|
187
207
|
[tokenName]: this.assets[tokenName].currency.baseUnit(amount),
|
|
@@ -219,7 +239,6 @@ export class SolanaWebsocketMonitor extends SolanaClarityMonitor {
|
|
|
219
239
|
const balance = this.#computeTotalBalance({ amount, address, stakingInfo, walletAccount })
|
|
220
240
|
|
|
221
241
|
const newData = {
|
|
222
|
-
// ...accountState, // we don't wanna update "tokenBalances" with old values (after sending an SPL token we have 2 events one updating SOL balance and one updating tokenBalances)
|
|
223
242
|
balance,
|
|
224
243
|
}
|
|
225
244
|
|