@exodus/solana-api 3.25.4 → 3.26.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/src/connection.js CHANGED
@@ -1,68 +1,39 @@
1
1
  import { WebSocket } from '@exodus/fetch'
2
2
  import debugLogger from 'debug'
3
3
  import delay from 'delay'
4
- import lodash from 'lodash'
5
- import makeConcurrent from 'make-concurrent'
4
+ import assert from 'minimalistic-assert'
6
5
  import ms from 'ms'
7
6
 
8
- // WS subscriptions: https://docs.solana.com/developing/clients/jsonrpc-api#subscription-websocket
9
-
10
- const SOLANA_DEFAULT_ENDPOINT = 'wss://solana.a.exodus.io/ws'
11
7
  const DEFAULT_RECONNECT_DELAY = ms('15s')
12
- const PING_INTERVAL = ms('60s')
13
- const TIMEOUT = ms('50s')
8
+ const PING_INTERVAL = ms('30s')
14
9
 
15
10
  const debug = debugLogger('exodus:solana-api')
16
11
 
17
12
  export class Connection {
18
13
  constructor({
19
- endpoint = SOLANA_DEFAULT_ENDPOINT,
14
+ endpoint,
20
15
  address,
21
16
  tokensAddresses = [],
22
- callback,
17
+ onConnectionReady,
18
+ onConnectionClose,
23
19
  onMsg,
24
- reconnectCallback = () => {},
25
- reconnectDelay = DEFAULT_RECONNECT_DELAY,
26
20
  }) {
21
+ assert(endpoint, 'endpoint is required')
27
22
  this.address = address
28
23
  this.tokensAddresses = tokensAddresses
29
24
  this.endpoint = endpoint
30
- this.callback = callback
25
+ this.onConnectionReady = onConnectionReady
26
+ this.onConnectionClose = onConnectionClose
31
27
  this.onMsg = onMsg
32
- this.reconnectCallback = reconnectCallback
33
- this.reconnectDelay = reconnectDelay
34
28
 
35
29
  this.shutdown = false
36
30
  this.ws = null
37
- this.rpcQueue = {}
38
31
  this.messageQueue = []
39
32
  this.inProcessMessages = false
40
33
  this.pingTimeout = null
41
34
  this.reconnectTimeout = null
42
35
  this.txCache = {}
43
36
  this.seq = 0
44
-
45
- this.sendMessage = makeConcurrent(
46
- async (method, params = []) => {
47
- return new Promise((resolve, reject) => {
48
- if (this.isClosed || this.shutdown) return reject(new Error('connection not started'))
49
- const id = ++this.seq
50
-
51
- this.rpcQueue[id] = { resolve, reject }
52
- this.rpcQueue[id].timeout = setTimeout(() => {
53
- delete this.rpcQueue[id]
54
- console.log(`solana ws: reply timeout (${method}) - ${JSON.stringify(params)} - ${id}`)
55
- resolve(null)
56
- }, TIMEOUT)
57
- if (typeof this.rpcQueue[id].timeout.unref === 'function') {
58
- this.rpcQueue[id].timeout.unref()
59
- }
60
-
61
- this.ws.send(JSON.stringify({ jsonrpc: '2.0', method, params, id }))
62
- })
63
- },
64
- { concurrency: 15 }
65
- )
66
37
  }
67
38
 
68
39
  newSocket(reqUrl) {
@@ -106,10 +77,25 @@ export class Connection {
106
77
  return 'NONE'
107
78
  }
108
79
 
109
- doPing() {
80
+ startPing() {
110
81
  if (this.ws) {
111
- this.ws.ping()
112
- this.pingTimeout = setTimeout(this.doPing.bind(this), PING_INTERVAL)
82
+ this.pingTimeout = setInterval(() => {
83
+ if (this.isOpen) {
84
+ if (typeof this.ws.ping === 'function') {
85
+ this.ws.ping()
86
+ } else {
87
+ // Some WebSocket implementations (like in browser or Electron renderer) don't have a ping method
88
+ this.ws.send(
89
+ JSON.stringify({
90
+ jsonrpc: '2.0',
91
+ id: Date.now(),
92
+ method: 'getVersion',
93
+ params: [],
94
+ })
95
+ )
96
+ }
97
+ }
98
+ }, PING_INTERVAL)
113
99
  }
114
100
  }
115
101
 
@@ -117,75 +103,35 @@ export class Connection {
117
103
  // debug('Restarting WS:')
118
104
  this.reconnectTimeout = setTimeout(async () => {
119
105
  try {
120
- debug('reconnecting ws...')
106
+ console.log('SOL reconnecting ws...')
121
107
  this.start()
122
- await this.reconnectCallback()
123
108
  } catch (e) {
124
109
  console.log(`Error in reconnect callback: ${e.message}`)
125
110
  }
126
- }, this.reconnectDelay)
111
+ }, DEFAULT_RECONNECT_DELAY)
127
112
  }
128
113
 
129
114
  onMessage(evt) {
130
115
  try {
131
116
  const json = JSON.parse(evt.data)
132
117
  debug('new ws msg:', json)
133
- if (json.error) {
134
- if (lodash.get(this.rpcQueue, json.id)) {
135
- this.rpcQueue[json.id].reject(new Error(json.error.message))
136
- clearTimeout(this.rpcQueue[json.id].timeout)
137
- delete this.rpcQueue[json.id]
138
- } else debug('Unsupported WS message:', json.error.message)
139
- } else {
140
- if (lodash.get(this.rpcQueue, json.id)) {
141
- // json-rpc reply
142
- clearTimeout(this.rpcQueue[json.id].timeout)
143
- this.rpcQueue[json.id].resolve(json.result)
144
- delete this.rpcQueue[json.id]
145
- } else if (json.method) {
146
- const msg = { method: json.method, ...lodash.get(json, 'params.result', json.result) }
147
- debug('pushing msg to queue', msg)
148
- this.messageQueue.push(msg) // sub results
149
- }
150
-
151
- this.processMessages(json)
152
- }
118
+ this.onMsg(json)
153
119
  } catch (e) {
154
120
  debug(e)
155
- debug('Cannot parse msg:', evt.data)
121
+ debug('Cannot process msg:', evt.data)
156
122
  }
157
123
  }
158
124
 
159
125
  onOpen(evt) {
160
126
  debug('Opened WS')
161
- // subscribe to each addresses (SOL and ASA addr)
162
- const addresses = [...this.tokensAddresses, this.address]
163
- addresses.forEach((address) => {
164
- // sub for account state changes
165
- this.ws.send(
166
- JSON.stringify({
167
- jsonrpc: '2.0',
168
- method: 'accountSubscribe',
169
- params: [
170
- address,
171
- {
172
- encoding: 'jsonParsed',
173
- },
174
- ],
175
- id: ++this.seq,
176
- })
177
- )
178
- // sub for incoming/outcoming txs
179
- this.ws.send(
180
- JSON.stringify({
181
- jsonrpc: '2.0',
182
- method: 'logsSubscribe',
183
- params: [{ mentions: [address] }, { commitment: 'finalized' }],
184
- id: ++this.seq,
185
- })
186
- )
187
- })
188
- // this.doPing()
127
+ this.onConnectionReady(evt)
128
+ this.startPing()
129
+ }
130
+
131
+ send(args) {
132
+ if (this.isOpen) {
133
+ this.ws.send(JSON.stringify(args))
134
+ }
189
135
  }
190
136
 
191
137
  onError(evt) {
@@ -194,32 +140,17 @@ export class Connection {
194
140
 
195
141
  onClose(evt) {
196
142
  debug('Closing WS')
197
- clearTimeout(this.pingTimeout)
143
+ clearInterval(this.pingTimeout)
198
144
  clearTimeout(this.reconnectTimeout)
145
+ this.onConnectionClose(evt)
199
146
  if (!this.shutdown) {
200
147
  this.doRestart()
201
148
  }
202
149
  }
203
150
 
204
- async processMessages(json) {
205
- if (this.onMsg) await this.onMsg(json)
206
- if (this.inProcessMessages) return null
207
- this.inProcessMessages = true
208
- try {
209
- while (this.messageQueue.length > 0) {
210
- const items = this.messageQueue.splice(0, this.messageQueue.length)
211
- await this.callback(items)
212
- }
213
- } catch (e) {
214
- console.log(`Solana: error processing streams: ${e.message}`)
215
- } finally {
216
- this.inProcessMessages = false
217
- }
218
- }
219
-
220
151
  async close() {
221
152
  clearTimeout(this.reconnectTimeout)
222
- clearTimeout(this.pingTimeout)
153
+ clearInterval(this.pingTimeout)
223
154
  if (this.ws && (this.isConnecting || this.isOpen)) {
224
155
  // this.ws.send(JSON.stringify({ method: 'close' }))
225
156
  // Not sending the method above so just no need to wait below
@@ -245,4 +176,4 @@ export class Connection {
245
176
  await this.close()
246
177
  while (this.running) await delay(ms('50ms'))
247
178
  }
248
- } // Connection
179
+ }
package/src/index.js CHANGED
@@ -6,6 +6,7 @@ import { Api } from './api.js'
6
6
 
7
7
  export { SolanaMonitor } from './tx-log/index.js'
8
8
  export { SolanaClarityMonitor } from './tx-log/index.js'
9
+ export { SolanaWebsocketMonitor } from './tx-log/index.js'
9
10
  export { createAccountState } from './account-state.js'
10
11
  export { getStakingInfo } from './staking-utils.js'
11
12
  export {
@@ -32,4 +33,5 @@ const serverApi = new Api({ assets }) // TODO: remove it, clean every use from p
32
33
  export default serverApi // TODO: remove it
33
34
 
34
35
  export { Api } from './api.js'
36
+ export { WsApi } from './ws-api.js'
35
37
  export { ClarityApi } from './clarity-api.js'
package/src/rpc-api.js CHANGED
@@ -112,14 +112,6 @@ export class RpcApi {
112
112
  return result?.value?.blockhash
113
113
  }
114
114
 
115
- async getPriorityFee(transaction) {
116
- // https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api
117
- const result = await this.rpcCall('getPriorityFeeEstimate', [
118
- { transaction, options: { recommended: true } },
119
- ])
120
- return result.priorityFeeEstimate
121
- }
122
-
123
115
  async getBlockTime(slot) {
124
116
  // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
125
117
  return this.rpcCall('getBlockTime', [slot])
@@ -0,0 +1,63 @@
1
+ # WS Clarity Monitor
2
+
3
+ This monitor will use a Mix of Clarity RPC calls and WS events (using Helius Enhanced Websockets: https://www.helius.dev/docs/enhanced-websockets)
4
+
5
+ ## Solana Monitor startup logic using WS and Clarity API
6
+
7
+ 1. on wallet first start we call API to get the current account state (balances + txsHistory + staking), for both SOL and tokens.
8
+
9
+ 2. we subscribe to WS for both SOL and tokens addresses (accountSubscribe, transactionSubscribe).
10
+
11
+ 3. after that we will rely only on WS to get new txs and update balances, but we will still ran regular tick to get a list of new "token accounts" (tokenAccountsByOwner), this to detect if the wallet has new ATA with new tokens. We could replace this with programSubscribe once Helius implements that method.
12
+
13
+ 4. when a new tx is received from WS we will parse it and add it to the tx log immediately.
14
+
15
+ 5. when a new balance update is received from WS we will update the balance immediately, (plus we also get and update stakingInfo).
16
+
17
+ 6. when a new tx is received, it is parsed and if it's a staking tx we update the stakingInfo in the state.
18
+
19
+ ### Notes
20
+
21
+ - when there's a balance we always fetch and update stakingInfo.
22
+
23
+ - Both Laserstream gRPC and Geyser enhanced websockets are serviced by Laserstream under the hood.
24
+
25
+ For `Richat`:
26
+ If you have 10 ATA you will have
27
+
28
+ - 11 `accountSubscribe` - 1 for wallet and 10 for ATA
29
+ - 1 `transactionSubscribe` - all 11 addresses in account.include
30
+ - 1 `tokenInitSubscribe` - 1 with wallet address
31
+
32
+ For `transactionSubscribe`, once you have new ATA unfortunately you need to send new `transactionSubscribe`, once you receive messages that subscription is set you need to unsubscribe the old subscription, at that moment you will be able to receive duplicated transactions for a short period of time.
33
+ (But this way you will not miss any transaction and will be easy to filter out duplicates on client side by txId)
34
+
35
+ Note that `tokenInitSubscribe` is a Richat-only method (in development). It doesn't exist in Laserstream/Geyser. It's used to check for new token accounts. For that we still need to rely on the `getTokenAccountsByOwner` RPC (or clarity call).
36
+
37
+ ## WS RPC
38
+
39
+ ##### `accountSubscribe` and `accountNotification`
40
+
41
+ When we are subscribed (`accountSubscribe`) to both SOL and SPL tokens and our account sends out an SPL token.
42
+
43
+ We'll have 2 `accountNotification`. Notifying us about the SPL balance change but also SOL balance change (because of the fees spent to send the SPL token).
44
+
45
+ How can we distinguish that? looking at the owner. For our SOL account the program owner is `11111111111111111111111111111111` while for tokens we have `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`
46
+
47
+ ### What to test in platform
48
+
49
+ - [x] check on wallet startup if the wallet tick updating balances and txHistory (for txs received when the wallet was close)
50
+ - [x] send/receive SOL token (got expected balance and txLog)
51
+ - [x] send/receive SPL token (got expected balance and txLog)
52
+ - [x] send/receive SPL2022 token, like PYUSD (got expected balance and txLog)
53
+ - [ ] receive SPL token that was not previously received (no token account yet)
54
+ - [x] staking/unstaking/withdraw SOL
55
+ - [x] force WS disconnect (check if the tokenAccounts are re-subscribed, apparently the onClose event is not called and the socket is still open)
56
+ - [x] disconnect wallet A -> send SOL from a different wallet B (connected) - re-connect A -> the wallet should catch up and see the balance and txLog
57
+ - [x] JUP swaps (and other DEX interactions?)
58
+
59
+ ### Known issues:
60
+
61
+ - [ ] when receiving a new token with no token account yet (the txLog is not written)
62
+
63
+ - if it's a known token (in this.assets) but no tokenAccount yet, the balance will update, txLog won't.
@@ -63,6 +63,8 @@ export class SolanaClarityMonitor extends BaseMonitor {
63
63
  if (unknownTokensList.length > 0) {
64
64
  this.emit('unknown-tokens', unknownTokensList)
65
65
  }
66
+
67
+ return unknownTokensList
66
68
  }
67
69
 
68
70
  async getStakingAddressesFromTxLog({ assetName, walletAccount }) {
@@ -126,13 +128,22 @@ export class SolanaClarityMonitor extends BaseMonitor {
126
128
  const isHistoryUpdateTick =
127
129
  this.tickCount[walletAccount] % this.ticksBetweenHistoryFetches === 0
128
130
 
129
- const shouldUpdateHistory = refresh || isHistoryUpdateTick || balanceChanged
131
+ const baseAssetTxLog = await this.aci.getTxLog({
132
+ assetName,
133
+ walletAccount,
134
+ })
135
+ const hasUnconfirmedSentTx = [...baseAssetTxLog].some((tx) => tx.pending && tx.sent)
136
+
137
+ const shouldUpdateHistory =
138
+ refresh || isHistoryUpdateTick || balanceChanged || hasUnconfirmedSentTx
130
139
  const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateHistory
131
140
 
141
+ // start a batch
142
+ const batch = this.aci.createOperationsBatch()
132
143
  // getHistory is more likely to fail/be rate limited, so we want to update users balance only on a lot of ticks
133
144
  if (this.shouldUpdateBalanceBeforeHistory || shouldUpdateOnlyBalance) {
134
145
  // update all state at once
135
- await this.updateState({ account, walletAccount, staking })
146
+ this.updateState({ account, walletAccount, staking, batch })
136
147
  await this.emitUnknownTokensEvent({ tokenAccounts })
137
148
  }
138
149
 
@@ -149,13 +160,21 @@ export class SolanaClarityMonitor extends BaseMonitor {
149
160
 
150
161
  // update all state at once
151
162
  const clearedLogItems = await this.markStaleTransactions({ walletAccount, logItemsByAsset })
152
- await this.updateTxLogByAsset({ walletAccount, logItemsByAsset: clearedLogItems, refresh })
153
- await this.updateState({ account, cursorState, walletAccount, staking })
163
+ this.updateTxLogByAssetBatch({
164
+ logItemsByAsset: clearedLogItems,
165
+ walletAccount,
166
+ refresh,
167
+ batch,
168
+ })
169
+ this.updateState({ account, cursorState, walletAccount, staking, batch })
154
170
  await this.emitUnknownTokensEvent({ tokenAccounts })
155
171
  if (refresh || cursorChanged) {
156
172
  this.cursors[walletAccount] = cursorState.cursor
157
173
  }
158
174
  }
175
+
176
+ // close batch
177
+ await this.aci.executeOperationsBatch(batch)
159
178
  }
160
179
 
161
180
  async getHistory({ address, accountState, refresh, tokenAccounts } = Object.create(null)) {
@@ -172,12 +191,10 @@ export class SolanaClarityMonitor extends BaseMonitor {
172
191
  const mappedTransactions = []
173
192
  for (const tx of transactions) {
174
193
  // we get the token name using the token.mintAddress
175
- let tokenName = this.api.tokens.get(tx.token?.mintAddress)?.name
176
- if (tx.token && !tokenName) {
177
- tokenName = 'unknown' // unknown token
178
- }
194
+ const assetName = tx.token
195
+ ? this.api.tokens.get(tx.token.mintAddress)?.name ?? 'unknown'
196
+ : baseAsset.name
179
197
 
180
- const assetName = tokenName ?? baseAsset.name
181
198
  const asset = this.assets[assetName]
182
199
  if (assetName === 'unknown' || !asset) continue // skip unknown tokens
183
200
  const feeAsset = asset.feeAsset
@@ -249,12 +266,10 @@ export class SolanaClarityMonitor extends BaseMonitor {
249
266
  }
250
267
 
251
268
  async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
252
- const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
253
269
  const [accountInfo, { balances: splBalances, accounts: tokenAccounts }] = await Promise.all([
254
270
  this.api.getAccountInfo(address).catch(() => {}),
255
271
  this.api.getTokensBalancesAndAccounts({
256
272
  address,
257
- filterByTokens: tokens,
258
273
  }),
259
274
  ])
260
275
 
@@ -321,7 +336,8 @@ export class SolanaClarityMonitor extends BaseMonitor {
321
336
  }
322
337
  }
323
338
 
324
- async updateState({ account, cursorState, walletAccount, staking }) {
339
+ updateState({ account, cursorState = {}, walletAccount, staking, batch }) {
340
+ const assetName = this.asset.name
325
341
  const { balance, tokenBalances, rentExemptAmount, accountSize, ownerChanged } = account
326
342
  const newData = {
327
343
  balance,
@@ -332,7 +348,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
332
348
  stakingInfo: staking,
333
349
  ...cursorState,
334
350
  }
335
- return this.updateAccountState({ newData, walletAccount })
351
+ return this.updateAccountStateBatch({ assetName, walletAccount, newData, batch })
336
352
  }
337
353
 
338
354
  async getStakingInfo({ address, accountState, walletAccount }) {
@@ -1,2 +1,3 @@
1
- export * from './solana-monitor.js'
2
- export * from './clarity-monitor.js'
1
+ export { SolanaMonitor } from './solana-monitor.js'
2
+ export { SolanaClarityMonitor } from './clarity-monitor.js'
3
+ export { SolanaWebsocketMonitor } from './ws-monitor.js'
@@ -7,7 +7,6 @@ import { DEFAULT_POOL_ADDRESS } from '../account-state.js'
7
7
 
8
8
  const DEFAULT_REMOTE_CONFIG = {
9
9
  rpcs: [],
10
- ws: [],
11
10
  staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS },
12
11
  }
13
12
 
@@ -81,9 +80,8 @@ export class SolanaMonitor extends BaseMonitor {
81
80
  }
82
81
 
83
82
  setServer(config = {}) {
84
- const { rpcs, ws, staking = {} } = { ...DEFAULT_REMOTE_CONFIG, ...config }
83
+ const { rpcs, staking = {} } = { ...DEFAULT_REMOTE_CONFIG, ...config }
85
84
  this.api.setServer(rpcs[0])
86
- this.api.setWsEndpoint(ws[0])
87
85
  this.staking = staking
88
86
  }
89
87
 
@@ -100,6 +98,8 @@ export class SolanaMonitor extends BaseMonitor {
100
98
  if (unknownTokensList.length > 0) {
101
99
  this.emit('unknown-tokens', unknownTokensList)
102
100
  }
101
+
102
+ return unknownTokensList
103
103
  }
104
104
 
105
105
  async getStakingAddressesFromTxLog({ assetName, walletAccount }) {
@@ -170,10 +170,12 @@ export class SolanaMonitor extends BaseMonitor {
170
170
  const shouldUpdateHistory = refresh || isHistoryUpdateTick || balanceChanged
171
171
  const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateHistory
172
172
 
173
+ // start a batch
174
+ const batch = this.aci.createOperationsBatch()
173
175
  // getHistory is more likely to fail/be rate limited, so we want to update users balance only on a lot of ticks
174
176
  if (this.shouldUpdateBalanceBeforeHistory || shouldUpdateOnlyBalance) {
175
177
  // update all state at once
176
- await this.updateState({ account, walletAccount, staking })
178
+ this.updateState({ account, walletAccount, staking, batch })
177
179
  await this.emitUnknownTokensEvent({ tokenAccounts })
178
180
  }
179
181
 
@@ -190,13 +192,21 @@ export class SolanaMonitor extends BaseMonitor {
190
192
 
191
193
  // update all state at once
192
194
  const clearedLogItems = await this.markStaleTransactions({ walletAccount, logItemsByAsset })
193
- await this.updateTxLogByAsset({ walletAccount, logItemsByAsset: clearedLogItems, refresh })
194
- await this.updateState({ account, cursorState, walletAccount, staking })
195
+ this.updateTxLogByAssetBatch({
196
+ logItemsByAsset: clearedLogItems,
197
+ walletAccount,
198
+ refresh,
199
+ batch,
200
+ })
201
+ this.updateState({ account, cursorState, walletAccount, staking, batch })
195
202
  await this.emitUnknownTokensEvent({ tokenAccounts })
196
203
  if (refresh || cursorChanged) {
197
204
  this.cursors[walletAccount] = cursorState.cursor
198
205
  }
199
206
  }
207
+
208
+ // close batch
209
+ await this.aci.executeOperationsBatch(batch)
200
210
  }
201
211
 
202
212
  async getHistory({ address, accountState, refresh, tokenAccounts } = Object.create(null)) {
@@ -212,7 +222,11 @@ export class SolanaMonitor extends BaseMonitor {
212
222
 
213
223
  const mappedTransactions = []
214
224
  for (const tx of transactions) {
215
- const assetName = _.get(tx, 'token.tokenName', baseAsset.name)
225
+ // we get the token name using the token.mintAddress
226
+ const assetName = tx.token
227
+ ? this.api.tokens.get(tx.token.mintAddress)?.name ?? 'unknown'
228
+ : baseAsset.name
229
+
216
230
  const asset = this.assets[assetName]
217
231
  if (assetName === 'unknown' || !asset) continue // skip unknown tokens
218
232
  const feeAsset = asset.feeAsset
@@ -262,11 +276,7 @@ export class SolanaMonitor extends BaseMonitor {
262
276
  item.data.meta = tx.data.meta
263
277
  }
264
278
 
265
- if (
266
- asset.assetType === this.api.tokenAssetType &&
267
- item.feeAmount &&
268
- item.feeAmount.isPositive
269
- ) {
279
+ if (asset.name !== asset.baseAsset.name && item.feeAmount && item.feeAmount.isPositive) {
270
280
  const feeItem = {
271
281
  ..._.clone(item),
272
282
  coinName: feeAsset.name,
@@ -352,7 +362,8 @@ export class SolanaMonitor extends BaseMonitor {
352
362
  }
353
363
  }
354
364
 
355
- async updateState({ account, cursorState, walletAccount, staking }) {
365
+ updateState({ account, cursorState = {}, walletAccount, staking, batch }) {
366
+ const assetName = this.asset.name
356
367
  const { balance, tokenBalances, rentExemptAmount, accountSize, ownerChanged } = account
357
368
  const newData = {
358
369
  balance,
@@ -363,7 +374,7 @@ export class SolanaMonitor extends BaseMonitor {
363
374
  stakingInfo: staking,
364
375
  ...cursorState,
365
376
  }
366
- return this.updateAccountState({ newData, walletAccount })
377
+ return this.updateAccountStateBatch({ assetName, walletAccount, newData, batch })
367
378
  }
368
379
 
369
380
  async getStakingInfo({ address, accountState, walletAccount }) {