@exodus/solana-api 3.8.2 → 3.9.0

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 CHANGED
@@ -3,6 +3,28 @@
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.9.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.8.3...@exodus/solana-api@3.9.0) (2024-07-08)
7
+
8
+
9
+ ### Features
10
+
11
+ * reuse SOL API results for getAccount and getTokenAccounts ([#2666](https://github.com/ExodusMovement/assets/issues/2666)) ([4e96f4c](https://github.com/ExodusMovement/assets/commit/4e96f4c66d7783b113f0cbdf73d30bb605ae0534))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * update SOL staking info on balance change ([#2672](https://github.com/ExodusMovement/assets/issues/2672)) ([bc2043c](https://github.com/ExodusMovement/assets/commit/bc2043ce226128d3e321937a325e20382f12f874))
17
+
18
+
19
+
20
+ ## [3.8.3](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.8.2...@exodus/solana-api@3.8.3) (2024-06-27)
21
+
22
+ **Note:** Version bump only for package @exodus/solana-api
23
+
24
+
25
+
26
+
27
+
6
28
  ## [3.8.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.8.1...@exodus/solana-api@3.8.2) (2024-06-27)
7
29
 
8
30
  **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.8.2",
3
+ "version": "3.9.0",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -32,6 +32,7 @@
32
32
  "@exodus/simple-retry": "^0.0.6",
33
33
  "@exodus/solana-lib": "^3.4.2",
34
34
  "@exodus/solana-meta": "^1.0.7",
35
+ "@exodus/timer": "^1.0.0",
35
36
  "bn.js": "^4.11.0",
36
37
  "debug": "^4.1.1",
37
38
  "delay": "^4.0.1",
@@ -46,7 +47,7 @@
46
47
  "@exodus/assets-testing": "^1.0.0",
47
48
  "@solana/web3.js": "^1.91.8"
48
49
  },
49
- "gitHead": "f4f1b639d0c5d6dbb24b9c5349759c1a802e50bd",
50
+ "gitHead": "4690c7dc577330b5d317d762972343660111495d",
50
51
  "bugs": {
51
52
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
52
53
  },
package/src/index.js CHANGED
@@ -5,7 +5,7 @@ import assetsList from '@exodus/solana-meta'
5
5
  import { Api } from './api'
6
6
 
7
7
  export { default as SolanaFeeMonitor } from './fee-monitor'
8
- export { SolanaMonitor } from './tx-log'
8
+ export { SolanaMonitor, SolanaAutoWithdrawMonitor } from './tx-log'
9
9
  export { createAccountState } from './account-state'
10
10
  export { getSolStakedFee, getStakingInfo, getUnstakingFee } from './staking-utils'
11
11
  export {
@@ -1 +1,2 @@
1
1
  export * from './solana-monitor'
2
+ export * from './solana-auto-withdraw-monitor'
@@ -53,9 +53,9 @@ export class MeSolanaMonitor extends SolanaMonitor {
53
53
  return !this.useMeMonitor
54
54
  }
55
55
 
56
- async getTokenAccounts({ address }) {
56
+ async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
57
57
  if (!this.useMeMonitor) {
58
- return super.getTokenAccounts({ address })
58
+ return super.getAccountsAndBalances({ refresh, address, accountState, walletAccount })
59
59
  }
60
60
 
61
61
  const tokens = this.getTokens()
@@ -68,75 +68,62 @@ export class MeSolanaMonitor extends SolanaMonitor {
68
68
 
69
69
  const { balances } = await this.request('v1/wallet/balances/fungible').post(body).json()
70
70
  const metadata = new Map()
71
- balances.forEach((balance) => {
72
- if (balance.asset?.id) {
73
- metadata.set(balance.asset.id, { imageURL: balance.image })
71
+
72
+ const account = {
73
+ balance: this.asset.currency.ZERO,
74
+ tokenBalances: {},
75
+ }
76
+ const tokenAccounts = []
77
+
78
+ balances.forEach((assetBalance) => {
79
+ const { asset: assetData, balance } = assetBalance
80
+ const mintAddress = assetData.mintAddress
81
+
82
+ if (assetBalance.asset?.id) {
83
+ metadata.set(assetBalance.asset.id, { imageURL: assetBalance.image })
74
84
  }
75
- })
76
- this.emit('token-metadata', { source: 'solana', metadata })
77
85
 
78
- return balances
79
- .filter((balance) => balance.asset.mintAddress !== SOL_NATIVE)
80
- .map((balance) => {
81
- const asset = balance.asset
82
- const mintAddress = asset.mintAddress
86
+ if (mintAddress === SOL_NATIVE) {
87
+ // SOL balance
88
+ account.balance = this.asset.currency.baseUnit(balance.rawBalance)
89
+ } else {
90
+ // Fungible token balances
83
91
  const token = tokens.get(mintAddress) || {
84
92
  // name here is the exodus unique identifier not the display name
85
93
  name: 'unknown',
86
- ticker: asset.symbol,
94
+ ticker: assetData.symbol,
87
95
  decimals: balance.decimals,
88
96
  }
89
97
 
90
- return {
91
- tokenAccountAddress: asset.tokenAccount,
92
- owner: balance.owner,
98
+ if (tokens.get(mintAddress)) {
99
+ const tokenKey = token.name
100
+ account.tokenBalances[tokenKey] = token.currency.baseUnit(balance.rawBalance)
101
+ }
102
+
103
+ const tokenAccount = {
104
+ tokenAccountAddress: assetData.tokenAccount,
105
+ owner: assetBalance.owner,
93
106
  tokenName: token.name,
94
107
  ticker: token.ticker,
95
- balance: balance.balance.rawBalance,
108
+ balance: assetBalance.balance.rawBalance,
96
109
  mintAddress,
97
- tokenProgram: asset.tokenProgram,
110
+ tokenProgram: assetData.tokenProgram,
98
111
  decimals: token.decimals,
99
- feeBasisPoints: asset.feeBasisPoints ?? 0,
100
- maximumFee: asset.maximumFee ?? 0,
112
+ feeBasisPoints: assetData.feeBasisPoints ?? 0,
113
+ maximumFee: assetData.maximumFee ?? 0,
101
114
  }
102
- })
103
- }
104
-
105
- async getAccount({ address, staking, tokenAccounts }) {
106
- if (!this.useMeMonitor) {
107
- return super.getAccount({ address, staking, tokenAccounts })
108
- }
109
-
110
- const tokens = this.getTokens()
111
- const body = [
112
- {
113
- address,
114
- chain: 'solana',
115
- },
116
- ]
117
-
118
- const { balances } = await this.request('v1/wallet/balances/fungible').post(body).json()
119
-
120
- const result = {
121
- balance: this.asset.currency.ZERO,
122
- tokenBalances: {},
123
- }
124
-
125
- balances.forEach((balanceData) => {
126
- const { asset: responseAsset, balance } = balanceData
127
- const mintAddress = responseAsset.mintAddress
128
-
129
- if (mintAddress === SOL_NATIVE) {
130
- // SOL balance
131
- result.balance = this.asset.currency.baseUnit(balance.rawBalance)
132
- } else if (tokens.has(mintAddress)) {
133
- // Fungible token balances
134
- const token = tokens.get(mintAddress)
135
- const tokenKey = token.name
136
- result.tokenBalances[tokenKey] = token.currency.baseUnit(balance.rawBalance)
115
+ tokenAccounts.push(tokenAccount)
137
116
  }
138
117
  })
118
+ this.emit('token-metadata', { source: 'solana', metadata })
119
+
120
+ const fetchStakingInfo =
121
+ refresh || this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
122
+ const staking =
123
+ this.isStakingEnabled() && fetchStakingInfo
124
+ ? await this.getStakingInfo({ address, walletAccount })
125
+ : { ...accountState.mem, staking: this.staking }
139
126
 
140
- return result
127
+ return { account, tokenAccounts, staking }
141
128
  }
142
129
  }
@@ -0,0 +1,81 @@
1
+ import { Timer } from '@exodus/timer'
2
+ import { get } from 'lodash'
3
+ import ms from 'ms'
4
+ import assert from 'assert'
5
+
6
+ const INTERVAL = ms('30s')
7
+
8
+ export class SolanaAutoWithdrawMonitor {
9
+ constructor({ interval = INTERVAL, assetClientInterface, createAndSendStake }) {
10
+ this.assetName = 'solana'
11
+ this.timer = new Timer(interval)
12
+ this.aci = assetClientInterface
13
+ this.createAndSendStake = createAndSendStake
14
+ assert(typeof createAndSendStake === 'function', 'createAndSendStake is required')
15
+ this.cursors = {}
16
+ }
17
+
18
+ start = async () => {
19
+ const assets = await this.aci.getAssetsForNetwork({ baseAssetName: this.assetName })
20
+ this.asset = assets[this.assetName]
21
+ await this.timer.start(() => this.tick())
22
+ }
23
+
24
+ async tick() {
25
+ const walletAccounts = await this.aci.getWalletAccounts({ assetName: this.assetName })
26
+ await Promise.all(walletAccounts.map((walletAccount) => this._tick({ walletAccount })))
27
+ }
28
+
29
+ async _tick({ walletAccount }) {
30
+ const accountState = await this.aci.getAccountState({
31
+ assetName: this.assetName,
32
+ walletAccount,
33
+ })
34
+ const { cursor, mem } = accountState
35
+ const { loaded, withdrawable } = mem
36
+
37
+ if (!Array.isArray(this.cursors[walletAccount])) this.cursors[walletAccount] = []
38
+ const cursorChanged = !this.cursors[walletAccount].includes(cursor)
39
+
40
+ if (loaded && cursorChanged && withdrawable.isPositive) {
41
+ this.cursors[walletAccount].push(cursor)
42
+ try {
43
+ const txIds = await this.tryWithdraw({ accountState, walletAccount })
44
+ this.cursors[walletAccount].push(...txIds)
45
+ } catch (e) {
46
+ console.log('solana auto withdraw error:', e)
47
+ }
48
+ }
49
+ }
50
+
51
+ async tryWithdraw({ accountState, walletAccount }) {
52
+ const stakingInfo = accountState.mem
53
+ const feeData = await this.aci.getFeeData({ assetName: this.assetName })
54
+ const fee = get(feeData, 'fee', this.asset.currency.ZERO)
55
+
56
+ const solBalance = get(accountState, 'balance', this.asset.currency.ZERO)
57
+ if (solBalance.lt(fee) || stakingInfo.withdrawable.isZero) return []
58
+
59
+ const promises = await this.createAndSendStake(
60
+ {
61
+ method: 'withdraw',
62
+ walletAccount,
63
+ amount: stakingInfo.withdrawable,
64
+ },
65
+ { watchForTxConfirmation: false }
66
+ )
67
+
68
+ return Promise.all(promises)
69
+ }
70
+ }
71
+
72
+ /*
73
+ const _solanaAutoWithdrawMonitor = new SolanaAutoWithdrawMonitor({ interval: INTERVAL })
74
+
75
+ export const solanaAutoWithdrawMonitor = {
76
+ start:
77
+ ({ assetClientInterface, createAndSendStake }) =>
78
+ async () =>
79
+ _solanaAutoWithdrawMonitor.start({ assetClientInterface, createAndSendStake }),
80
+ }
81
+ */
@@ -134,14 +134,23 @@ export class SolanaMonitor extends BaseMonitor {
134
134
  return clearedLogItems
135
135
  }
136
136
 
137
- async getTokenAccounts({ address }) {
138
- return this.api.getTokenAccountsByOwner(address)
139
- }
140
-
141
137
  isStakingEnabled() {
142
138
  return true
143
139
  }
144
140
 
141
+ async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
142
+ const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
143
+ const { account, staking } = await this.getAccount({
144
+ refresh,
145
+ address,
146
+ tokenAccounts,
147
+ accountState,
148
+ walletAccount,
149
+ })
150
+
151
+ return { account, tokenAccounts, staking }
152
+ }
153
+
145
154
  async tick({ walletAccount, refresh }) {
146
155
  // Check for new wallet account
147
156
  await this.initWalletAccount({ walletAccount })
@@ -152,17 +161,13 @@ export class SolanaMonitor extends BaseMonitor {
152
161
 
153
162
  const accountState = await this.aci.getAccountState({ assetName, walletAccount })
154
163
  const address = await this.aci.getReceiveAddress({ assetName, walletAccount, useCache: true })
155
- const stakingAddresses = await this.getStakingAddressesFromTxLog({ assetName, walletAccount })
156
-
157
- const fetchStakingInfo = this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
158
- const staking =
159
- this.isStakingEnabled() && fetchStakingInfo
160
- ? await this.getStakingInfo({ address, stakingAddresses })
161
- : { ...accountState.mem, staking: this.staking }
162
-
163
- const tokenAccounts = await this.getTokenAccounts({ address })
164
- const account = await this.getAccount({ address, staking, tokenAccounts })
165
164
 
165
+ const { account, tokenAccounts, staking } = await this.getAccountsAndBalances({
166
+ refresh,
167
+ address,
168
+ accountState,
169
+ walletAccount,
170
+ })
166
171
  const balanceChanged = this.#balanceChanged({ account: accountState, newAccount: account })
167
172
 
168
173
  const isHistoryUpdateTick =
@@ -278,13 +283,34 @@ export class SolanaMonitor extends BaseMonitor {
278
283
  }
279
284
  }
280
285
 
281
- async getAccount({ address, staking, tokenAccounts }) {
286
+ async getAccount({ refresh, address, tokenAccounts, accountState, walletAccount }) {
282
287
  const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
283
288
  const [solBalance, splBalances] = await Promise.all([
284
289
  this.api.getBalance(address),
285
290
  this.api.getTokensBalance({ address, filterByTokens: tokens, tokenAccounts }),
286
291
  ])
287
292
 
293
+ const tokenBalances = _.mapValues(splBalances, (balance, name) =>
294
+ this.assets[name].currency.baseUnit(balance).toDefault()
295
+ )
296
+
297
+ const solBalanceChanged = this.#balanceChanged({
298
+ account: accountState,
299
+ newAccount: {
300
+ balance: this.asset.currency.baseUnit(solBalance), // balance without staking
301
+ tokenBalances,
302
+ },
303
+ })
304
+ const fetchStakingInfo =
305
+ refresh ||
306
+ solBalanceChanged ||
307
+ this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
308
+
309
+ const staking =
310
+ this.isStakingEnabled() && fetchStakingInfo
311
+ ? await this.getStakingInfo({ address, walletAccount })
312
+ : { ...accountState.mem, staking: this.staking }
313
+
288
314
  const stakedBalance = this.asset.currency.baseUnit(staking.locked)
289
315
  const withdrawableBalance = this.asset.currency.baseUnit(staking.withdrawable)
290
316
  const pendingBalance = this.asset.currency.baseUnit(staking.pending)
@@ -295,13 +321,12 @@ export class SolanaMonitor extends BaseMonitor {
295
321
  .add(pendingBalance)
296
322
  .toDefault()
297
323
 
298
- const tokenBalances = _.mapValues(splBalances, (balance, name) =>
299
- this.assets[name].currency.baseUnit(balance).toDefault()
300
- )
301
-
302
324
  return {
303
- balance,
304
- tokenBalances,
325
+ account: {
326
+ balance,
327
+ tokenBalances,
328
+ },
329
+ staking,
305
330
  }
306
331
  }
307
332
 
@@ -311,8 +336,12 @@ export class SolanaMonitor extends BaseMonitor {
311
336
  return this.updateAccountState({ newData, walletAccount })
312
337
  }
313
338
 
314
- async getStakingInfo({ address, stakingAddresses = [] }) {
339
+ async getStakingInfo({ address, walletAccount }) {
315
340
  const stakingInfo = await this.api.getStakeAccountsInfo(address)
341
+ const stakingAddresses = await this.getStakingAddressesFromTxLog({
342
+ assetName: this.asset.name,
343
+ walletAccount,
344
+ })
316
345
  // merge current and old staking addresses
317
346
  const allStakingAddresses = _.uniq([...Object.keys(stakingInfo.accounts), ...stakingAddresses])
318
347
  const rewards = await this.api.getRewards(allStakingAddresses)