@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "2.5.31-alpha.2",
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 (FORCE_HTTP) return false
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().slice(0, TXS_LIMIT) // merge arrays
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.concat(val.instructions)
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 = !!tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
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(tx)
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(tx)
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.find((errorMessage) => error.message.includes(errorMessage))
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.onmessage = this.onMessage.bind(this)
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.onerror = this.onError.bind(this)
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
- this.tokensAddresses.concat(this.address).forEach((address) => {
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
- // OPTIONAL. Relying on polling through ws
57
- tokensAddresses: [], // needed for ASA subs
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 fetchStakingInfo = this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
133
- const staking = fetchStakingInfo
134
- ? await this.getStakingInfo({ address, stakingAddresses })
135
- : accountState.mem
136
-
137
- const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
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 balanceChanged = this.balanceChanged({ account: accountState, newAccount: account })
109
+ const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
141
110
 
142
- const isHistoryUpdateTick =
143
- this.tickCount[walletAccount] % this.ticksBetweenHistoryFetches === 0
111
+ if (refresh || hasNewTxs || cursorChanged) {
112
+ const staking =
113
+ refresh || cursorChanged
114
+ ? await this.getStakingInfo({ address, stakingAddresses })
115
+ : accountState.mem
144
116
 
145
- const shouldUpdateHistory = refresh || isHistoryUpdateTick || balanceChanged
146
- const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateHistory
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
  }