@exodus/solana-api 2.0.4 → 2.0.5

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.
@@ -0,0 +1 @@
1
+ export * from './solana-monitor'
@@ -0,0 +1,189 @@
1
+ import { BaseMonitor } from '@exodus/asset-lib'
2
+ import _ from 'lodash'
3
+ import assert from 'minimalistic-assert'
4
+
5
+ const DEFAULT_POOL_ADDRESS = '9QU2QSxhb24FUX3Tu2FpczXjpK3VYrvRudywSZaM29mF' // Everstake
6
+
7
+ const DEFAULT_REMOTE_CONFIG = { rpcs: [], staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS } }
8
+
9
+ export class SolanaMonitor extends BaseMonitor {
10
+ constructor({ api, includeUnparsed = false, ...args }) {
11
+ super(args)
12
+ assert(api, 'api is required')
13
+ this.api = api
14
+ this.cursors = {}
15
+ this.assets = {}
16
+ this.includeUnparsed = includeUnparsed
17
+ }
18
+
19
+ setServer(config = {}) {
20
+ const { rpcs, staking = {} } = { ...DEFAULT_REMOTE_CONFIG, ...config }
21
+ this.api.setServer(rpcs[0])
22
+ this.staking = staking
23
+ }
24
+
25
+ hasNewCursor({ walletAccount, cursorState }) {
26
+ const { cursor } = cursorState
27
+ return this.cursors[walletAccount] !== cursor
28
+ }
29
+
30
+ async emitUnknownTokensEvent({ tokenAccounts }) {
31
+ const tokensList = await this.api.getWalletTokensList({ tokenAccounts })
32
+ if (tokensList.length > 0) {
33
+ this.emit('unknown-tokens', tokensList)
34
+ }
35
+ }
36
+
37
+ async tick({ walletAccount, refresh }) {
38
+ const assetName = this.asset.name
39
+ this.assets = await this.aci.getAssetsForNetwork({ baseAssetName: assetName })
40
+ this.api.setTokens(this.assets)
41
+
42
+ const accountState = await this.aci.getAccountState({ assetName, walletAccount })
43
+ const address = await this.aci.getReceiveAddress({ assetName, walletAccount })
44
+
45
+ const { logItemsByAsset, hasNewTxs, cursorState } = await this.getHistory({
46
+ address,
47
+ accountState,
48
+ walletAccount,
49
+ refresh,
50
+ })
51
+
52
+ let staking = accountState.mem
53
+
54
+ const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
55
+ if (refresh || cursorChanged) {
56
+ staking = await this.updateStakingInfo({ walletAccount, address })
57
+ this.cursors[walletAccount] = cursorState.cursor
58
+ }
59
+
60
+ await this.updateTxLogByAsset({ walletAccount, logItemsByAsset, refresh })
61
+ if (refresh || hasNewTxs || cursorChanged) {
62
+ const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
63
+ await this.emitUnknownTokensEvent({ tokenAccounts })
64
+ const account = await this.getAccount({ address, staking, tokenAccounts })
65
+ await this.updateState({ account, cursorState, walletAccount })
66
+ }
67
+ }
68
+
69
+ async getHistory({ address, accountState, refresh } = {}) {
70
+ let cursor = refresh ? '' : accountState.cursor
71
+ const baseAsset = this.asset
72
+
73
+ const { transactions, newCursor } = await this.api.getTransactions(address, {
74
+ cursor,
75
+ includeUnparsed: this.includeUnparsed,
76
+ })
77
+
78
+ const mappedTransactions = []
79
+ for (const tx of transactions) {
80
+ const assetName = _.get(tx, 'token.tokenName', baseAsset.name)
81
+ const asset = this.assets[assetName]
82
+ if (assetName === 'unknown' || !asset) continue // skip unknown tokens
83
+
84
+ const coinAmount = asset.currency.baseUnit(tx.amount).toDefault()
85
+
86
+ const item = {
87
+ coinName: assetName,
88
+ txId: tx.id,
89
+ from: [tx.from],
90
+ coinAmount,
91
+ confirmations: 1, // tx.confirmations, // avoid multiple notifications
92
+ date: tx.date,
93
+ error: tx.error,
94
+ data: {
95
+ staking: tx.staking || null,
96
+ unparsed: !!tx.unparsed,
97
+ },
98
+ }
99
+
100
+ if (tx.owner === address) {
101
+ // send transaction
102
+ item.to = tx.to
103
+ item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault() // in SOL
104
+ item.coinAmount = item.coinAmount.negate()
105
+
106
+ if (tx.to === tx.owner) {
107
+ item.selfSend = true
108
+ item.coinAmount = asset.currency.ZERO
109
+ }
110
+ } else if (tx.unparsed) {
111
+ if (tx.fee !== 0) item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault() // in SOL
112
+
113
+ item.data.meta = tx.data.meta
114
+ }
115
+ if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
116
+ const feeAsset = asset.feeAsset
117
+ const feeItem = {
118
+ ..._.clone(item),
119
+ coinName: feeAsset.name,
120
+ tokens: [asset.name],
121
+ coinAmount: feeAsset.currency.ZERO,
122
+ }
123
+ mappedTransactions.push(feeItem)
124
+ }
125
+ mappedTransactions.push(item)
126
+ }
127
+
128
+ // logItemsByAsset = { 'solana:': [...], 'serum': [...] }
129
+
130
+ return {
131
+ logItemsByAsset: _.groupBy(mappedTransactions, (item) => item.coinName),
132
+ hasNewTxs: transactions.length > 0,
133
+ cursorState: { cursor: newCursor },
134
+ }
135
+ }
136
+
137
+ async getAccount({ address, staking, tokenAccounts }) {
138
+ const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
139
+ const [solBalance, splBalances] = await Promise.all([
140
+ this.api.getBalance(address),
141
+ this.api.getTokensBalance({ address, filterByTokens: tokens, tokenAccounts }),
142
+ ])
143
+
144
+ const stakedBalance = this.asset.currency.baseUnit(staking.locked).toDefault()
145
+ const withdrawableBalance = this.asset.currency.baseUnit(staking.withdrawable).toDefault()
146
+ const pendingBalance = this.asset.currency.baseUnit(staking.pending).toDefault()
147
+ const balance = this.asset.currency
148
+ .baseUnit(solBalance)
149
+ .toDefault()
150
+ .add(stakedBalance)
151
+ .add(withdrawableBalance)
152
+ .add(pendingBalance)
153
+
154
+ const tokenBalances = _.mapValues(splBalances, (balance, name) =>
155
+ this.assets[name].currency.baseUnit(balance).toDefault()
156
+ )
157
+
158
+ return {
159
+ balance,
160
+ tokenBalances,
161
+ }
162
+ }
163
+
164
+ async updateState({ account, cursorState, walletAccount }) {
165
+ const { balance, tokenBalances } = account
166
+ const newData = { balance, tokenBalances, ...cursorState }
167
+ return this.updateAccountState({ newData, walletAccount })
168
+ }
169
+
170
+ async updateStakingInfo({ walletAccount, address }) {
171
+ const stakingInfo = await this.api.getStakeAccountsInfo(address)
172
+ const rewards = await this.api.getRewards(Object.keys(stakingInfo.accounts))
173
+ const mem = {
174
+ loaded: true,
175
+ staking: this.staking,
176
+ isDelegating: Object.values(stakingInfo.accounts).some(({ state }) =>
177
+ ['active', 'activating', 'inactive'].includes(state)
178
+ ), // true if at least 1 account is delegating
179
+ locked: this.asset.currency.baseUnit(stakingInfo.locked).toDefault(),
180
+ withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable).toDefault(),
181
+ pending: this.asset.currency.baseUnit(stakingInfo.pending).toDefault(), // still undelegating (not yet available for withdraw)
182
+ earned: this.asset.currency.baseUnit(rewards).toDefault(),
183
+ accounts: stakingInfo.accounts, // Obj
184
+ }
185
+
186
+ await this.updateAccountState({ walletAccount, newData: { mem } })
187
+ return mem
188
+ }
189
+ }