@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.
- package/package.json +8 -4
- package/src/account-state.js +32 -0
- package/src/api.js +863 -0
- package/src/index.js +3 -863
- package/src/tx-log/index.js +1 -0
- package/src/tx-log/solana-monitor.js +189 -0
|
@@ -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
|
+
}
|