@exodus/solana-api 3.25.4 → 3.26.1

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/src/ws-api.js ADDED
@@ -0,0 +1,263 @@
1
+ import { PublicKey, Token, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, U64 } from '@exodus/solana-lib'
2
+ import lodash from 'lodash'
3
+
4
+ import { Connection } from './connection.js'
5
+ import { parseTransaction } from './tx-parser.js'
6
+
7
+ // Helius Advanced WebSocket
8
+ const WS_ENDPOINT = 'wss://solana-helius-wss.a.exodus.io/ws' // pointing to: wss://atlas-mainnet.helius-rpc.com/?api-key=<API_KEY>
9
+
10
+ export class WsApi {
11
+ constructor({ rpcUrl, wsUrl, assets }) {
12
+ this.setWsEndpoint(wsUrl)
13
+ this.connections = Object.create(null)
14
+ this.accountSubscriptions = Object.create(null)
15
+ }
16
+
17
+ setWsEndpoint(wsUrl) {
18
+ this.wsUrl = wsUrl || WS_ENDPOINT
19
+ }
20
+
21
+ async watchAddress({ address, tokensAddresses = [], onMessage }) {
22
+ if (this.connections[address]) return // already subscribed
23
+ const conn = new Connection({
24
+ endpoint: this.wsUrl,
25
+ address,
26
+ tokensAddresses,
27
+ onConnectionReady: (evt) => {
28
+ // ws connected, can send subscribe requests (this is called on every re-connect as well)
29
+ console.log('SOL WS connected.')
30
+ this.sendSubscriptions({ address, tokensAddresses })
31
+ },
32
+ onConnectionClose: (evt) => {
33
+ this.accountSubscriptions = Object.create(null) // clear subs
34
+ },
35
+ onMsg: (json) => onMessage(json),
36
+ })
37
+
38
+ this.connections[address] = conn
39
+ return this.connections[address].start()
40
+ }
41
+
42
+ async accountSubscribe({ owner, account }) {
43
+ // could be SOL address or token account address
44
+ const conn = this.connections[owner]
45
+ if (!conn || !conn.isOpen) {
46
+ console.warn('SOL Connection is not open, cannot subscribe to', owner)
47
+ return
48
+ }
49
+
50
+ const subscriptions = this.accountSubscriptions[owner] || []
51
+ if (subscriptions?.includes(account)) return // already subscribed
52
+
53
+ conn.send({
54
+ jsonrpc: '2.0',
55
+ method: 'accountSubscribe',
56
+ params: [
57
+ account,
58
+ {
59
+ encoding: 'jsonParsed',
60
+ commitment: 'confirmed',
61
+ },
62
+ ],
63
+ id: ++conn.seq,
64
+ })
65
+
66
+ this.accountSubscriptions[owner] = [...subscriptions, account]
67
+ }
68
+
69
+ async sendSubscriptions({ address, tokensAddresses = [] }) {
70
+ const conn = this.connections[address]
71
+
72
+ const addresses = [address, ...tokensAddresses]
73
+
74
+ // 1. subscribe to each addresses (SOL and Token addresses) balance events
75
+ // transform this forEach in a for of to await each subscription:
76
+ for (const addr of addresses) {
77
+ await this.accountSubscribe({ owner: address, account: addr })
78
+ }
79
+
80
+ // 2. subscribe to transactions involving the addresses
81
+ if (conn) {
82
+ conn.send({
83
+ jsonrpc: '2.0',
84
+ id: ++conn.seq,
85
+ method: 'transactionSubscribe',
86
+ params: [
87
+ {
88
+ vote: false,
89
+ // failed: true,
90
+ accountInclude: addresses,
91
+ },
92
+ {
93
+ commitment: 'confirmed',
94
+ encoding: 'jsonParsed',
95
+ transactionDetails: 'full',
96
+ showRewards: false,
97
+ maxSupportedTransactionVersion: 255,
98
+ },
99
+ ],
100
+ })
101
+ }
102
+
103
+ // 3. subscribe to other events, for example use programSubscribe once Helius implements it into the Advanced WebSocket
104
+ // to get the new token accounts events and remove the need for the RPC call in the monitor tick
105
+ }
106
+
107
+ async unwatchAddress({ address }) {
108
+ if (this.connections[address]) {
109
+ await this.connections[address].stop()
110
+ delete this.connections[address]
111
+ }
112
+ }
113
+
114
+ parseAccountNotification({ address, walletAccount, tokenAccountsByOwner, result }) {
115
+ const isSolAccount = result.value.owner === '11111111111111111111111111111111' // System Program
116
+ if (isSolAccount) {
117
+ // SOL balance changed
118
+ const amount = result.value.lamports
119
+ return { solAddress: address, amount }
120
+ }
121
+
122
+ const isSplTokenAccount = result.value.owner === TOKEN_PROGRAM_ID.toBase58()
123
+ const isSpl2022TokenAccount = result.value.owner === TOKEN_2022_PROGRAM_ID.toBase58()
124
+
125
+ // SPL Token balance changed (both spl-token and spl-2022 have the first 165 bytes the same)
126
+ if (isSplTokenAccount || isSpl2022TokenAccount) {
127
+ const decoded = Token.decode(Buffer.from(result.value.data[0], 'base64'))
128
+ const tokenMintAddress = new PublicKey(decoded.mint).toBase58()
129
+ const solAddress = new PublicKey(decoded.owner).toBase58()
130
+ const amount = U64.fromBuffer(decoded.amount).toString()
131
+
132
+ return {
133
+ solAddress,
134
+ amount,
135
+ tokenMintAddress,
136
+ }
137
+ }
138
+ }
139
+
140
+ parseTransactionNotification({
141
+ address,
142
+ walletAccount,
143
+ baseAsset,
144
+ assets,
145
+ tokens,
146
+ tokenAccountsByOwner,
147
+ result,
148
+ }) {
149
+ const parsedTx = parseTransaction(address, result.transaction, tokenAccountsByOwner)
150
+ const timestamp = Date.now() // the notification event has no blockTime
151
+
152
+ if (!parsedTx.from && parsedTx.tokenTxs?.length === 0) return { logItemsByAsset: {} } // cannot parse it
153
+
154
+ const transactions = [] // because one single tx can have multiple instructions inside
155
+
156
+ if (parsedTx.dexTxs) {
157
+ parsedTx.dexTxs.forEach((tx) => {
158
+ transactions.push({
159
+ timestamp,
160
+ date: new Date(timestamp),
161
+ ...tx,
162
+ })
163
+ })
164
+ delete parsedTx.dexTxs
165
+ }
166
+
167
+ if (parsedTx.tokenTxs?.length > 0) {
168
+ parsedTx.tokenTxs.forEach((tx) => {
169
+ transactions.push({
170
+ timestamp,
171
+ date: new Date(timestamp),
172
+ ...tx,
173
+ })
174
+ })
175
+ delete parsedTx.tokenTxs
176
+ }
177
+
178
+ if (parsedTx.from) {
179
+ transactions.push({
180
+ timestamp,
181
+ date: new Date(timestamp),
182
+ ...parsedTx,
183
+ })
184
+ }
185
+
186
+ const mappedTransactions = []
187
+ for (const tx of transactions) {
188
+ // we get the token name using the token.mintAddress
189
+ let tokenName = tokens.get(tx.token?.mintAddress)?.name
190
+ if (tx.token && !tokenName) {
191
+ tokenName = 'unknown' // unknown token
192
+ }
193
+
194
+ const assetName = tokenName ?? baseAsset.name
195
+ const asset = assets[assetName]
196
+ if (assetName === 'unknown' || !asset) return // skip unknown tokens
197
+ const feeAsset = asset.feeAsset
198
+
199
+ const coinAmount = tx.amount ? asset.currency.baseUnit(tx.amount) : asset.currency.ZERO
200
+
201
+ const item = {
202
+ coinName: assetName,
203
+ txId: tx.id,
204
+ from: [tx.from],
205
+ coinAmount,
206
+ confirmations: 1, // tx.confirmations, // avoid multiple notifications
207
+ date: tx.date,
208
+ error: tx.error,
209
+ data: {
210
+ staking: tx.staking || null,
211
+ unparsed: !!tx.unparsed,
212
+ swapTx: !!(tx.data && tx.data.inner),
213
+ },
214
+ currencies: { [assetName]: asset.currency, [feeAsset.name]: feeAsset.currency },
215
+ }
216
+
217
+ if (tx.owner === address) {
218
+ // send transaction
219
+ item.to = Array.isArray(tx.to) ? undefined : tx.to
220
+ item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
221
+ item.feeCoinName = baseAsset.name
222
+ item.coinAmount = item.coinAmount.negate()
223
+
224
+ if (tx.data?.sent) {
225
+ item.data.sent = tx.data.sent.map((s) => ({
226
+ address: s.address,
227
+ amount: asset.currency.baseUnit(s.amount).toDefaultString({ unit: true }),
228
+ }))
229
+ }
230
+
231
+ if (tx.to === tx.owner) {
232
+ item.selfSend = true
233
+ item.coinAmount = asset.currency.ZERO
234
+ }
235
+ } else if (tx.unparsed) {
236
+ if (tx.fee !== 0) {
237
+ item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
238
+ item.feeCoinName = baseAsset.name
239
+ }
240
+
241
+ item.data.meta = tx.data.meta
242
+ }
243
+
244
+ if (asset.name !== asset.baseAsset.name && item.feeAmount && item.feeAmount.isPositive) {
245
+ const feeItem = {
246
+ ...lodash.clone(item),
247
+ coinName: feeAsset.name,
248
+ tokens: [asset.name],
249
+ coinAmount: feeAsset.currency.ZERO,
250
+ }
251
+ mappedTransactions.push(feeItem)
252
+ }
253
+
254
+ mappedTransactions.push(item)
255
+ }
256
+
257
+ const logItemsByAsset = lodash.groupBy(mappedTransactions, (item) => item.coinName)
258
+ return {
259
+ logItemsByAsset,
260
+ cursorState: transactions[0]?.id ? { cursor: transactions[0].id } : {},
261
+ }
262
+ }
263
+ }