@exodus/solana-api 2.5.31-alpha.2 → 2.5.31
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/package.json +6 -1
- package/src/api.js +12 -8
- package/src/connection.js +9 -6
- package/src/tx-log/solana-monitor.js +18 -63
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.5.31
|
|
3
|
+
"version": "2.5.31",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"@exodus/asset-lib": "^4.0.0",
|
|
25
25
|
"@exodus/assets": "^9.0.1",
|
|
26
26
|
"@exodus/basic-utils": "^2.1.0",
|
|
27
|
+
"@exodus/currency": "^2.3.2",
|
|
27
28
|
"@exodus/fetch": "^1.2.0",
|
|
28
29
|
"@exodus/models": "^10.1.0",
|
|
29
30
|
"@exodus/nfts-core": "^0.5.0",
|
|
@@ -32,7 +33,11 @@
|
|
|
32
33
|
"@exodus/solana-meta": "^1.0.7",
|
|
33
34
|
"bn.js": "^4.11.0",
|
|
34
35
|
"debug": "^4.1.1",
|
|
36
|
+
"delay": "^4.0.1",
|
|
35
37
|
"lodash": "^4.17.11",
|
|
38
|
+
"make-concurrent": "^4.0.0",
|
|
39
|
+
"minimalistic-assert": "^1.0.1",
|
|
40
|
+
"ms": "^2.1.3",
|
|
36
41
|
"url-join": "4.0.0",
|
|
37
42
|
"wretch": "^1.5.2"
|
|
38
43
|
},
|
package/src/api.js
CHANGED
|
@@ -25,7 +25,6 @@ import { Connection } from './connection'
|
|
|
25
25
|
const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com
|
|
26
26
|
const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // not standard across all node providers (we're compatible only with Quicknode)
|
|
27
27
|
const FORCE_HTTP = true // use https over ws
|
|
28
|
-
const TXS_LIMIT = 100
|
|
29
28
|
|
|
30
29
|
// Tokens + SOL api support
|
|
31
30
|
export class Api {
|
|
@@ -60,16 +59,18 @@ export class Api {
|
|
|
60
59
|
async watchAddress({
|
|
61
60
|
address,
|
|
62
61
|
tokensAddresses = [],
|
|
62
|
+
onMessage,
|
|
63
63
|
handleAccounts,
|
|
64
64
|
handleTransfers,
|
|
65
65
|
handleReconnect,
|
|
66
66
|
reconnectDelay,
|
|
67
67
|
}) {
|
|
68
|
-
if (
|
|
68
|
+
if (this.connections[address]) return // already subscribed
|
|
69
69
|
const conn = new Connection({
|
|
70
70
|
endpoint: this.wsUrl,
|
|
71
71
|
address,
|
|
72
72
|
tokensAddresses,
|
|
73
|
+
onMsg: (json) => onMessage(json),
|
|
73
74
|
callback: (updates) =>
|
|
74
75
|
this.handleUpdates({ updates, address, handleAccounts, handleTransfers }),
|
|
75
76
|
reconnectCallback: handleReconnect,
|
|
@@ -187,7 +188,7 @@ export class Api {
|
|
|
187
188
|
})
|
|
188
189
|
)
|
|
189
190
|
)
|
|
190
|
-
let txsId = txsResultsByAccount.flat()
|
|
191
|
+
let txsId = txsResultsByAccount.flat() // merge arrays
|
|
191
192
|
txsId = lodash.uniqBy(txsId, 'signature')
|
|
192
193
|
|
|
193
194
|
// get txs details in parallel
|
|
@@ -334,7 +335,7 @@ export class Api {
|
|
|
334
335
|
}))
|
|
335
336
|
innerInstructions = innerInstructions
|
|
336
337
|
.reduce((acc, val) => {
|
|
337
|
-
return acc
|
|
338
|
+
return [...acc, ...val.instructions]
|
|
338
339
|
}, [])
|
|
339
340
|
.map((ix) => {
|
|
340
341
|
const type = lodash.get(ix, 'parsed.type')
|
|
@@ -350,7 +351,7 @@ export class Api {
|
|
|
350
351
|
const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
351
352
|
return [source, destination].includes(tokenAccountAddress)
|
|
352
353
|
})
|
|
353
|
-
const isSending =
|
|
354
|
+
const isSending = tokenAccountsByOwner.some(({ tokenAccountAddress }) => {
|
|
354
355
|
return [source].includes(tokenAccountAddress)
|
|
355
356
|
})
|
|
356
357
|
|
|
@@ -486,9 +487,11 @@ export class Api {
|
|
|
486
487
|
const accountIndexes = lodash.mapKeys(accountKeys, (x, i) => i)
|
|
487
488
|
Object.values(accountIndexes).forEach((acc) => {
|
|
488
489
|
// filter by ownerAddress
|
|
490
|
+
// eslint-disable-next-line unicorn/prefer-array-some
|
|
489
491
|
const hasKnownOwner = !!lodash.find(tokenAccountsByOwner, {
|
|
490
492
|
tokenAccountAddress: acc.pubkey,
|
|
491
493
|
})
|
|
494
|
+
// eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
|
|
492
495
|
acc.owner = hasKnownOwner ? ownerAddress : null
|
|
493
496
|
})
|
|
494
497
|
|
|
@@ -512,7 +515,7 @@ export class Api {
|
|
|
512
515
|
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
513
516
|
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
514
517
|
// SOL->SPL swaps on Raydium and Orca.
|
|
515
|
-
tx = getUnparsedTx(
|
|
518
|
+
tx = getUnparsedTx()
|
|
516
519
|
tx.dexTxs = getInnerTxsFromBalanceChanges()
|
|
517
520
|
} else {
|
|
518
521
|
if (solanaTx) {
|
|
@@ -543,7 +546,7 @@ export class Api {
|
|
|
543
546
|
const unparsed = Object.keys(tx).length === 0
|
|
544
547
|
|
|
545
548
|
if (unparsed && includeUnparsed) {
|
|
546
|
-
tx = getUnparsedTx(
|
|
549
|
+
tx = getUnparsedTx()
|
|
547
550
|
}
|
|
548
551
|
|
|
549
552
|
// How tokens tx are parsed:
|
|
@@ -821,7 +824,7 @@ export class Api {
|
|
|
821
824
|
} catch (error) {
|
|
822
825
|
if (
|
|
823
826
|
error.message &&
|
|
824
|
-
!errorMessagesToRetry.
|
|
827
|
+
!errorMessagesToRetry.some((errorMessage) => error.message.includes(errorMessage))
|
|
825
828
|
) {
|
|
826
829
|
error.finalError = true
|
|
827
830
|
}
|
|
@@ -966,6 +969,7 @@ export class Api {
|
|
|
966
969
|
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
967
970
|
transactionMessage || message
|
|
968
971
|
)
|
|
972
|
+
// eslint-disable-next-line unicorn/no-new-array
|
|
969
973
|
const signatures = new Array(message.header.numRequiredSignatures || 1).fill(null)
|
|
970
974
|
const encodedTransaction = buildRawTransaction(
|
|
971
975
|
Buffer.from(message.serialize()),
|
package/src/connection.js
CHANGED
|
@@ -20,6 +20,7 @@ export class Connection {
|
|
|
20
20
|
address,
|
|
21
21
|
tokensAddresses = [],
|
|
22
22
|
callback,
|
|
23
|
+
onMsg,
|
|
23
24
|
reconnectCallback = () => {},
|
|
24
25
|
reconnectDelay = DEFAULT_RECONNECT_DELAY,
|
|
25
26
|
}) {
|
|
@@ -27,6 +28,7 @@ export class Connection {
|
|
|
27
28
|
this.tokensAddresses = tokensAddresses
|
|
28
29
|
this.endpoint = endpoint
|
|
29
30
|
this.callback = callback
|
|
31
|
+
this.onMsg = onMsg
|
|
30
32
|
this.reconnectCallback = reconnectCallback
|
|
31
33
|
this.reconnectDelay = reconnectDelay
|
|
32
34
|
|
|
@@ -66,10 +68,10 @@ export class Connection {
|
|
|
66
68
|
reqUrl = `${obj}`
|
|
67
69
|
debug('Opening WS to:', reqUrl)
|
|
68
70
|
const ws = new WebSocket(`${reqUrl}`)
|
|
69
|
-
ws.
|
|
71
|
+
ws.addEventListener('message', this.onMessage.bind(this))
|
|
70
72
|
ws.addEventListener('open', this.onOpen.bind(this))
|
|
71
73
|
ws.addEventListener('close', this.onClose.bind(this))
|
|
72
|
-
ws.
|
|
74
|
+
ws.addEventListener('error', this.onError.bind(this))
|
|
73
75
|
return ws
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -143,7 +145,7 @@ export class Connection {
|
|
|
143
145
|
this.messageQueue.push(msg) // sub results
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
this.processMessages()
|
|
148
|
+
this.processMessages(json)
|
|
147
149
|
}
|
|
148
150
|
} catch (e) {
|
|
149
151
|
debug(e)
|
|
@@ -154,8 +156,8 @@ export class Connection {
|
|
|
154
156
|
onOpen(evt) {
|
|
155
157
|
debug('Opened WS')
|
|
156
158
|
// subscribe to each addresses (SOL and ASA addr)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
const addresses = [...this.tokensAddresses, this.address]
|
|
160
|
+
addresses.forEach((address) => {
|
|
159
161
|
// sub for account state changes
|
|
160
162
|
this.ws.send(
|
|
161
163
|
JSON.stringify({
|
|
@@ -196,7 +198,8 @@ export class Connection {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
async processMessages() {
|
|
201
|
+
async processMessages(json) {
|
|
202
|
+
if (this.onMsg) await this.onMsg(json)
|
|
200
203
|
if (this.inProcessMessages) return null
|
|
201
204
|
this.inProcessMessages = true
|
|
202
205
|
try {
|
|
@@ -10,25 +10,14 @@ const DEFAULT_REMOTE_CONFIG = {
|
|
|
10
10
|
staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS },
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const TICKS_BETWEEN_HISTORY_FETCHES = 10
|
|
14
|
-
const TICKS_BETWEEN_STAKE_FETCHES = 5
|
|
15
|
-
|
|
16
13
|
export class SolanaMonitor extends BaseMonitor {
|
|
17
|
-
constructor({
|
|
18
|
-
api,
|
|
19
|
-
includeUnparsed = false,
|
|
20
|
-
ticksBetweenHistoryFetches = TICKS_BETWEEN_HISTORY_FETCHES,
|
|
21
|
-
ticksBetweenStakeFetches = TICKS_BETWEEN_STAKE_FETCHES,
|
|
22
|
-
...args
|
|
23
|
-
}) {
|
|
14
|
+
constructor({ api, includeUnparsed = false, ...args }) {
|
|
24
15
|
super(args)
|
|
25
16
|
assert(api, 'api is required')
|
|
26
17
|
this.api = api
|
|
27
18
|
this.cursors = {}
|
|
28
19
|
this.assets = {}
|
|
29
20
|
this.staking = DEFAULT_REMOTE_CONFIG.staking
|
|
30
|
-
this.ticksBetweenStakeFetches = ticksBetweenStakeFetches
|
|
31
|
-
this.ticksBetweenHistoryFetches = ticksBetweenHistoryFetches
|
|
32
21
|
this.includeUnparsed = includeUnparsed
|
|
33
22
|
this.addHook('before-stop', (...args) => this.beforeStop(...args))
|
|
34
23
|
}
|
|
@@ -52,15 +41,10 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
52
41
|
})
|
|
53
42
|
return this.api.watchAddress({
|
|
54
43
|
address,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
handleAccounts: (updates) => this.accountsCallback({ updates, walletAccount }),
|
|
59
|
-
handleTransfers: (txs) => {
|
|
60
|
-
// new SOL tx, ticking monitor
|
|
61
|
-
this.tick({ walletAccount }) // it will cause refresh for both sender/receiver. Without necessarily fetching the tx if it's not finalized in the node.
|
|
44
|
+
onMessage: (json) => {
|
|
45
|
+
// new SOL tx event, tick monitor with 15 sec delay (to avoid hitting delayed nodes)
|
|
46
|
+
setTimeout(() => this.tick({ walletAccount }), 15_000)
|
|
62
47
|
},
|
|
63
|
-
*/
|
|
64
48
|
})
|
|
65
49
|
}
|
|
66
50
|
|
|
@@ -103,20 +87,6 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
103
87
|
return _.uniq(stakingAddresses.flat())
|
|
104
88
|
}
|
|
105
89
|
|
|
106
|
-
balanceChanged({ account, newAccount }) {
|
|
107
|
-
const solBalanceChanged = !account.balance || !account.balance.equals(newAccount.balance)
|
|
108
|
-
if (solBalanceChanged) return true
|
|
109
|
-
|
|
110
|
-
const tokenBalanceChanged =
|
|
111
|
-
!account.tokenBalances ||
|
|
112
|
-
Object.entries(newAccount.tokenBalances).some(
|
|
113
|
-
([token, balance]) =>
|
|
114
|
-
!account.tokenBalances[token] || !account.tokenBalances[token].equals(balance)
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
return tokenBalanceChanged
|
|
118
|
-
}
|
|
119
|
-
|
|
120
90
|
async tick({ walletAccount, refresh }) {
|
|
121
91
|
// Check for new wallet account
|
|
122
92
|
await this.initWalletAccount({ walletAccount })
|
|
@@ -129,43 +99,28 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
129
99
|
const address = await this.aci.getReceiveAddress({ assetName, walletAccount, useCache: true })
|
|
130
100
|
const stakingAddresses = await this.getStakingAddressesFromTxLog({ assetName, walletAccount })
|
|
131
101
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const account = await this.getAccount({ address, staking, tokenAccounts })
|
|
102
|
+
const { logItemsByAsset, hasNewTxs, cursorState } = await this.getHistory({
|
|
103
|
+
address,
|
|
104
|
+
accountState,
|
|
105
|
+
walletAccount,
|
|
106
|
+
refresh,
|
|
107
|
+
})
|
|
139
108
|
|
|
140
|
-
const
|
|
109
|
+
const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
|
|
141
110
|
|
|
142
|
-
|
|
143
|
-
|
|
111
|
+
if (refresh || hasNewTxs || cursorChanged) {
|
|
112
|
+
const staking =
|
|
113
|
+
refresh || cursorChanged
|
|
114
|
+
? await this.getStakingInfo({ address, stakingAddresses })
|
|
115
|
+
: accountState.mem
|
|
144
116
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const shouldUpdateBalanceBeforeHistory = true
|
|
117
|
+
const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
|
|
118
|
+
const account = await this.getAccount({ address, staking, tokenAccounts })
|
|
148
119
|
|
|
149
|
-
// getHistory is more likely to fail/be rate limited, so we want to update users balance only on a lot of ticks
|
|
150
|
-
if (shouldUpdateBalanceBeforeHistory || shouldUpdateOnlyBalance) {
|
|
151
120
|
// update all state at once
|
|
152
|
-
await this.updateState({ account, walletAccount, staking })
|
|
153
121
|
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
154
|
-
}
|
|
155
|
-
if (shouldUpdateHistory) {
|
|
156
|
-
const { logItemsByAsset, cursorState } = await this.getHistory({
|
|
157
|
-
address,
|
|
158
|
-
accountState,
|
|
159
|
-
walletAccount,
|
|
160
|
-
refresh,
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
|
|
164
|
-
|
|
165
|
-
// update all state at once
|
|
166
122
|
await this.updateTxLogByAsset({ walletAccount, logItemsByAsset, refresh })
|
|
167
123
|
await this.updateState({ account, cursorState, walletAccount, staking })
|
|
168
|
-
await this.emitUnknownTokensEvent({ tokenAccounts })
|
|
169
124
|
if (refresh || cursorChanged) {
|
|
170
125
|
this.cursors[walletAccount] = cursorState.cursor
|
|
171
126
|
}
|