@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/CHANGELOG.md +30 -0
- package/package.json +3 -3
- package/src/api.js +27 -632
- package/src/connection.js +42 -111
- package/src/index.js +2 -0
- package/src/rpc-api.js +0 -8
- package/src/tx-log/README.md +63 -0
- package/src/tx-log/clarity-monitor.js +29 -13
- package/src/tx-log/index.js +3 -2
- package/src/tx-log/solana-monitor.js +25 -14
- package/src/tx-log/ws-monitor.js +390 -0
- package/src/tx-parser.js +533 -0
- package/src/ws-api.js +263 -0
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
|
|
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('
|
|
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
|
|
14
|
+
endpoint,
|
|
20
15
|
address,
|
|
21
16
|
tokensAddresses = [],
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
80
|
+
startPing() {
|
|
110
81
|
if (this.ws) {
|
|
111
|
-
this.
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
121
|
+
debug('Cannot process msg:', evt.data)
|
|
156
122
|
}
|
|
157
123
|
}
|
|
158
124
|
|
|
159
125
|
onOpen(evt) {
|
|
160
126
|
debug('Opened WS')
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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.
|
|
351
|
+
return this.updateAccountStateBatch({ assetName, walletAccount, newData, batch })
|
|
336
352
|
}
|
|
337
353
|
|
|
338
354
|
async getStakingInfo({ address, accountState, walletAccount }) {
|
package/src/tx-log/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
377
|
+
return this.updateAccountStateBatch({ assetName, walletAccount, newData, batch })
|
|
367
378
|
}
|
|
368
379
|
|
|
369
380
|
async getStakingInfo({ address, accountState, walletAccount }) {
|