@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 +22 -0
- package/package.json +3 -2
- package/src/index.js +1 -1
- package/src/tx-log/index.js +1 -0
- package/src/tx-log/me-solana-monitor.js +43 -56
- package/src/tx-log/solana-auto-withdraw-monitor.js +81 -0
- package/src/tx-log/solana-monitor.js +51 -22
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.
|
|
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": "
|
|
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 {
|
package/src/tx-log/index.js
CHANGED
|
@@ -53,9 +53,9 @@ export class MeSolanaMonitor extends SolanaMonitor {
|
|
|
53
53
|
return !this.useMeMonitor
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async
|
|
56
|
+
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
57
57
|
if (!this.useMeMonitor) {
|
|
58
|
-
return super.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
94
|
+
ticker: assetData.symbol,
|
|
87
95
|
decimals: balance.decimals,
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
108
|
+
balance: assetBalance.balance.rawBalance,
|
|
96
109
|
mintAddress,
|
|
97
|
-
tokenProgram:
|
|
110
|
+
tokenProgram: assetData.tokenProgram,
|
|
98
111
|
decimals: token.decimals,
|
|
99
|
-
feeBasisPoints:
|
|
100
|
-
maximumFee:
|
|
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
|
|
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({
|
|
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
|
-
|
|
304
|
-
|
|
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,
|
|
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)
|