@exodus/solana-api 2.5.20 → 2.5.21

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/lib/api.js DELETED
@@ -1,1161 +0,0 @@
1
- 'use strict'
2
-
3
- Object.defineProperty(exports, '__esModule', {
4
- value: true,
5
- })
6
- exports.Api = void 0
7
-
8
- var _bn = _interopRequireDefault(require('bn.js'))
9
-
10
- var _assetJsonRpc = _interopRequireDefault(require('@exodus/asset-json-rpc'))
11
-
12
- var _simpleRetry = require('@exodus/simple-retry')
13
-
14
- var _fetch = require('@exodus/fetch')
15
-
16
- var _solanaLib = require('@exodus/solana-lib')
17
-
18
- var _assets = _interopRequireDefault(require('@exodus/assets'))
19
-
20
- var _assert = _interopRequireDefault(require('assert'))
21
-
22
- var _lodash = _interopRequireDefault(require('lodash'))
23
-
24
- var _urlJoin = _interopRequireDefault(require('url-join'))
25
-
26
- var _wretch = _interopRequireWildcard(require('wretch'))
27
-
28
- var _nftsCore = require('@exodus/nfts-core')
29
-
30
- var _connection = require('./connection')
31
-
32
- function _getRequireWildcardCache() {
33
- if (typeof WeakMap !== 'function') return null
34
- var cache = new WeakMap()
35
- _getRequireWildcardCache = function() {
36
- return cache
37
- }
38
- return cache
39
- }
40
-
41
- function _interopRequireWildcard(obj) {
42
- if (obj && obj.__esModule) {
43
- return obj
44
- }
45
- if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
46
- return { default: obj }
47
- }
48
- var cache = _getRequireWildcardCache()
49
- if (cache && cache.has(obj)) {
50
- return cache.get(obj)
51
- }
52
- var newObj = {}
53
- var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor
54
- for (var key in obj) {
55
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
56
- var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null
57
- if (desc && (desc.get || desc.set)) {
58
- Object.defineProperty(newObj, key, desc)
59
- } else {
60
- newObj[key] = obj[key]
61
- }
62
- }
63
- }
64
- newObj.default = obj
65
- if (cache) {
66
- cache.set(obj, newObj)
67
- }
68
- return newObj
69
- }
70
-
71
- function _interopRequireDefault(obj) {
72
- return obj && obj.__esModule ? obj : { default: obj }
73
- }
74
-
75
- const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com, https://solana-api.projectserum.com
76
-
77
- const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // Tokens + SOL api support
78
-
79
- class Api {
80
- constructor(rpcUrl, wsUrl) {
81
- this.broadcastTransaction = async (signedTx, options) => {
82
- console.log('Solana broadcasting TX:', signedTx) // base64
83
-
84
- const defaultOptions = {
85
- encoding: 'base64',
86
- preflightCommitment: 'finalized',
87
- }
88
- const params = [signedTx, { ...defaultOptions, ...options }]
89
- const errorMessagesToRetry = ['Blockhash not found']
90
- const broadcastTxWithRetry = (0, _simpleRetry.retry)(
91
- async () => {
92
- try {
93
- const result = await this.rpcCall('sendTransaction', params, {
94
- forceHttp: true,
95
- })
96
- console.log(`tx ${JSON.stringify(result)} sent!`)
97
- return result || null
98
- } catch (error) {
99
- if (
100
- error.message &&
101
- !errorMessagesToRetry.find((errorMessage) => error.message.includes(errorMessage))
102
- ) {
103
- error.finalError = true
104
- }
105
-
106
- console.warn(`Error broadcasting tx. Retrying...`, error)
107
- throw error
108
- }
109
- },
110
- {
111
- delayTimesMs: ['6s', '6s', '8s', '10s'],
112
- }
113
- )
114
- return broadcastTxWithRetry()
115
- }
116
-
117
- this.simulateTransaction = async (encodedTransaction, options) => {
118
- const {
119
- value: { accounts },
120
- } = await this.rpcCall('simulateTransaction', [encodedTransaction, options])
121
- return accounts
122
- }
123
-
124
- this.resolveSimulationSideEffects = async (solAccounts, tokenAccounts) => {
125
- const willReceive = []
126
- const willSend = []
127
- const resolveSols = solAccounts.map(async (account) => {
128
- const currentAmount = await this.getBalance(account.address)
129
- const balance = (0, _solanaLib.computeBalance)(account.amount, currentAmount)
130
- return {
131
- name: 'SOL',
132
- symbol: 'SOL',
133
- balance,
134
- decimal: _solanaLib.SOL_DECIMAL,
135
- type: 'SOL',
136
- }
137
- })
138
-
139
- const _wrapAndHandleAccountNotFound = (fn, defaultValue) => {
140
- return async (...params) => {
141
- try {
142
- return await fn.apply(this, params)
143
- } catch (error) {
144
- if (error.message && error.message.includes('could not find account')) {
145
- return defaultValue
146
- }
147
-
148
- throw error
149
- }
150
- }
151
- }
152
-
153
- const _getTokenBalance = _wrapAndHandleAccountNotFound(this.getTokenBalance, '0')
154
-
155
- const _getDecimals = _wrapAndHandleAccountNotFound(this.getDecimals, 0)
156
-
157
- const _getSupply = _wrapAndHandleAccountNotFound(this.getSupply, '0')
158
-
159
- const resolveTokens = tokenAccounts.map(async (account) => {
160
- try {
161
- const [_tokenMetaPlex, currentAmount, decimal] = await Promise.all([
162
- this.getMetaplexMetadata(account.mint),
163
- _getTokenBalance(account.address),
164
- _getDecimals(account.mint),
165
- ])
166
- const tokenMetaPlex = _tokenMetaPlex || {
167
- name: null,
168
- symbol: null,
169
- }
170
- let nft = {
171
- collectionId: null,
172
- collectionName: null,
173
- collectionTitle: null,
174
- title: null,
175
- } // Only perform an NFT check (getSupply) if decimal is zero
176
-
177
- if (decimal === 0 && (await _getSupply(account.mint)) === '1') {
178
- try {
179
- const {
180
- id: collectionId,
181
- collectionName,
182
- collectionTitle,
183
- title,
184
- } = await _nftsCore.magicEden.api.getNFTByMintAddress(account.mint)
185
- nft = {
186
- collectionId,
187
- collectionTitle,
188
- collectionName,
189
- title,
190
- }
191
- tokenMetaPlex.name = tokenMetaPlex.name || collectionTitle
192
- tokenMetaPlex.symbol = tokenMetaPlex.symbol || collectionName
193
- } catch (error) {
194
- console.warn(error)
195
- }
196
- }
197
-
198
- const balance = (0, _solanaLib.computeBalance)(account.amount, currentAmount)
199
- return {
200
- balance,
201
- decimal,
202
- nft,
203
- address: account.address,
204
- mint: account.mint,
205
- name: tokenMetaPlex.name,
206
- symbol: tokenMetaPlex.symbol,
207
- type: 'TOKEN',
208
- }
209
- } catch (error) {
210
- console.warn(error)
211
- return {
212
- balance: null,
213
- }
214
- }
215
- })
216
- const accounts = await Promise.all([...resolveSols, ...resolveTokens])
217
- accounts.forEach((account) => {
218
- if (account.balance === null) {
219
- return
220
- }
221
-
222
- if (account.balance > 0) {
223
- willReceive.push(account)
224
- } else {
225
- willSend.push(account)
226
- }
227
- })
228
- return {
229
- willReceive,
230
- willSend,
231
- }
232
- }
233
-
234
- this.simulateAndRetrieveSideEffects = async (message, publicKey, transactionMessage) => {
235
- const { config, accountAddresses } = (0, _solanaLib.getTransactionSimulationParams)(
236
- transactionMessage || message
237
- )
238
- const signatures = new Array(message.header.numRequiredSignatures || 1).fill(null)
239
- const encodedTransaction = (0, _solanaLib.buildRawTransaction)(
240
- Buffer.from(message.serialize()),
241
- signatures
242
- ).toString('base64')
243
- const futureAccountsState = await this.simulateTransaction(encodedTransaction, config)
244
- const { solAccounts, tokenAccounts } = (0, _solanaLib.filterAccountsByOwner)(
245
- futureAccountsState,
246
- accountAddresses,
247
- publicKey
248
- )
249
- return this.resolveSimulationSideEffects(solAccounts, tokenAccounts)
250
- }
251
-
252
- this.simulateTransactionV1 = async (network, transactions, publicKey, origin) => {
253
- ;(0, _assert.default)(
254
- ['mainnet', 'devnet', 'testnet'].includes(network),
255
- 'invalid network provided'
256
- )
257
- ;(0, _assert.default)(Array.isArray(transactions), 'transactions was not an array')
258
- ;(0, _assert.default)(typeof origin === 'string', 'origin must be a string')
259
- const body = JSON.stringify({
260
- transactions,
261
- userAccount: publicKey,
262
- metadata: {
263
- origin,
264
- },
265
- })
266
- const response = await (0, _fetch.fetch)(
267
- `https://api.blowfish.xyz/solana/v0/${network}/scan/transactions`,
268
- {
269
- method: 'POST',
270
- headers: {
271
- 'Content-Type': 'application/json',
272
- 'X-Api-Key': 'afd98abb-c464-413a-8947-6c29e6b6e393',
273
- },
274
- body,
275
- }
276
- )
277
-
278
- if (!response.ok) {
279
- throw new Error(await response.text())
280
- }
281
-
282
- return response.json()
283
- }
284
-
285
- this.setServer(rpcUrl)
286
- this.setWsEndpoint(wsUrl)
287
- this.setTokens(_assets.default)
288
- this.tokensToSkip = {}
289
- this.connections = {}
290
- }
291
-
292
- setServer(rpcUrl) {
293
- this.rpcUrl = rpcUrl || RPC_URL
294
- this.api = (0, _assetJsonRpc.default)(this.rpcUrl)
295
- }
296
-
297
- setWsEndpoint(wsUrl) {
298
- this.wsUrl = wsUrl || WS_ENDPOINT
299
- }
300
-
301
- setTokens(assets = {}) {
302
- const solTokens = _lodash.default.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
303
-
304
- this.tokens = _lodash.default.mapKeys(solTokens, (v) => v.mintAddress)
305
- }
306
-
307
- request(path, contentType = 'application/json') {
308
- return (0, _wretch.default)((0, _urlJoin.default)(this.rpcUrl, path)).headers({
309
- 'Content-type': contentType,
310
- })
311
- }
312
-
313
- async watchAddress({
314
- address,
315
- tokensAddresses = [],
316
- handleAccounts,
317
- handleTransfers,
318
- handleReconnect,
319
- reconnectDelay,
320
- }) {
321
- const conn = new _connection.Connection({
322
- endpoint: this.wsUrl,
323
- address,
324
- tokensAddresses,
325
- callback: (updates) =>
326
- this.handleUpdates({
327
- updates,
328
- address,
329
- handleAccounts,
330
- handleTransfers,
331
- }),
332
- reconnectCallback: handleReconnect,
333
- reconnectDelay,
334
- })
335
- this.connections[address] = conn
336
- return conn.start()
337
- }
338
-
339
- async unwatchAddress({ address }) {
340
- if (this.connections[address]) {
341
- await this.connections[address].stop()
342
- delete this.connections[address]
343
- }
344
- }
345
-
346
- async handleUpdates({ updates, address, handleAccounts, handleTransfers }) {
347
- // console.log(`got ws updates from ${address}:`, updates)
348
- if (handleTransfers) return handleTransfers(updates)
349
- }
350
-
351
- async rpcCall(method, params = [], { address = '', forceHttp = false } = {}) {
352
- // ws request
353
- const connection =
354
- this.connections[address] || _lodash.default.sample(Object.values(this.connections)) // pick random connection
355
-
356
- if (
357
- _lodash.default.get(connection, 'isOpen') &&
358
- !_lodash.default.get(connection, 'shutdown') &&
359
- !forceHttp
360
- ) {
361
- return connection.sendMessage(method, params)
362
- } // http fallback
363
-
364
- return this.api.post({
365
- method,
366
- params,
367
- })
368
- }
369
-
370
- getTokenByAddress(mint) {
371
- return this.tokens[mint]
372
- }
373
-
374
- isTokenSupported(mint) {
375
- return !!this.getTokenByAddress(mint)
376
- }
377
-
378
- async getEpochInfo() {
379
- const { epoch } = await this.rpcCall('getEpochInfo')
380
- return Number(epoch)
381
- }
382
-
383
- async getStakeActivation(address) {
384
- const { state } = await this.rpcCall('getStakeActivation', [address])
385
- return state
386
- }
387
-
388
- async getRecentBlockHash(commitment) {
389
- const result = await this.rpcCall(
390
- 'getRecentBlockhash',
391
- [
392
- {
393
- commitment: commitment || 'finalized',
394
- encoding: 'jsonParsed',
395
- },
396
- ],
397
- {
398
- forceHttp: true,
399
- }
400
- )
401
- return _lodash.default.get(result, 'value.blockhash')
402
- } // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
403
-
404
- async getTransactionById(id) {
405
- return this.rpcCall('getTransaction', [
406
- id,
407
- {
408
- encoding: 'jsonParsed',
409
- maxSupportedTransactionVersion: 0,
410
- },
411
- ])
412
- }
413
-
414
- async getFee() {
415
- const result = await this.rpcCall('getRecentBlockhash', [
416
- {
417
- commitment: 'finalized',
418
- encoding: 'jsonParsed',
419
- },
420
- ])
421
- return _lodash.default.get(result, 'value.feeCalculator.lamportsPerSignature')
422
- }
423
-
424
- async getBalance(address) {
425
- const result = await this.rpcCall(
426
- 'getBalance',
427
- [
428
- address,
429
- {
430
- encoding: 'jsonParsed',
431
- },
432
- ],
433
- {
434
- address,
435
- }
436
- )
437
- return _lodash.default.get(result, 'value', 0)
438
- }
439
-
440
- async getBlockTime(slot) {
441
- // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
442
- return this.rpcCall('getBlockTime', [slot])
443
- }
444
-
445
- async getConfirmedSignaturesForAddress(address, { until, before, limit } = {}) {
446
- until = until || undefined
447
- return this.rpcCall(
448
- 'getSignaturesForAddress',
449
- [
450
- address,
451
- {
452
- until,
453
- before,
454
- limit,
455
- },
456
- ],
457
- {
458
- address,
459
- }
460
- )
461
- }
462
- /**
463
- * Get transactions from an address
464
- */
465
-
466
- async getTransactions(address, { cursor, before, limit, includeUnparsed = false } = {}) {
467
- let transactions = [] // cursor is a txHash
468
-
469
- try {
470
- let until = cursor
471
- const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
472
-
473
- const tokenAccountAddresses = tokenAccountsByOwner
474
- .filter(({ tokenName }) => tokenName !== 'unknown')
475
- .map(({ tokenAccountAddress }) => tokenAccountAddress)
476
- const accountsToCheck = [address, ...tokenAccountAddresses]
477
- const txsResultsByAccount = await Promise.all(
478
- accountsToCheck.map((addr) =>
479
- this.getConfirmedSignaturesForAddress(addr, {
480
- until,
481
- before,
482
- limit,
483
- })
484
- )
485
- )
486
- let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []) // merge arrays
487
-
488
- txsId = _lodash.default.uniqBy(txsId, 'signature') // get txs details in parallel
489
-
490
- const txsDetails = await Promise.all(txsId.map((tx) => this.getTransactionById(tx.signature)))
491
- txsDetails.forEach((txDetail) => {
492
- if (txDetail === null) return
493
- const timestamp = txDetail.blockTime * 1000
494
- const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner, {
495
- includeUnparsed,
496
- })
497
- if (!parsedTx.from && !includeUnparsed) return // cannot parse it
498
- // split dexTx in separate txs
499
-
500
- if (parsedTx.dexTxs) {
501
- parsedTx.dexTxs.forEach((tx) => {
502
- transactions.push({
503
- timestamp,
504
- date: new Date(timestamp),
505
- ...tx,
506
- })
507
- })
508
- delete parsedTx.dexTxs
509
- }
510
-
511
- transactions.push({
512
- timestamp,
513
- date: new Date(timestamp),
514
- ...parsedTx,
515
- })
516
- })
517
- } catch (err) {
518
- console.warn('Solana error:', err)
519
- throw err
520
- }
521
-
522
- transactions = _lodash.default.orderBy(transactions, ['timestamp'], ['desc'])
523
- const newCursor = transactions[0] ? transactions[0].id : cursor
524
- return {
525
- transactions,
526
- newCursor,
527
- }
528
- }
529
-
530
- parseTransaction(
531
- ownerAddress,
532
- txDetails,
533
- tokenAccountsByOwner,
534
- { includeUnparsed = false } = {}
535
- ) {
536
- let {
537
- fee,
538
- preBalances,
539
- postBalances,
540
- preTokenBalances,
541
- postTokenBalances,
542
- innerInstructions,
543
- } = txDetails.meta
544
- preBalances = preBalances || []
545
- postBalances = postBalances || []
546
- preTokenBalances = preTokenBalances || []
547
- postTokenBalances = postTokenBalances || []
548
- innerInstructions = innerInstructions || []
549
- let { instructions, accountKeys } = txDetails.transaction.message
550
-
551
- const getUnparsedTx = () => {
552
- const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
553
- const feePaid = ownerIndex === 0 ? fee : 0
554
- return {
555
- unparsed: true,
556
- amount:
557
- ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
558
- fee: feePaid,
559
- data: {
560
- meta: txDetails.meta,
561
- },
562
- }
563
- }
564
-
565
- const getInnerTxsFromBalanceChanges = () => {
566
- const ownPreTokenBalances = preTokenBalances.filter(
567
- (balance) => balance.owner === ownerAddress
568
- )
569
- const ownPostTokenBalances = postTokenBalances.filter(
570
- (balance) => balance.owner === ownerAddress
571
- )
572
- return ownPostTokenBalances
573
- .map((postBalance) => {
574
- const tokenAccount = tokenAccountsByOwner.find(
575
- (tokenAccount) => tokenAccount.mintAddress === postBalance.mint
576
- )
577
- const preBalance = ownPreTokenBalances.find(
578
- (balance) => balance.accountIndex === postBalance.accountIndex
579
- )
580
- const preAmount = new _bn.default(
581
- _lodash.default.get(preBalance, 'uiTokenAmount.amount', '0'),
582
- 10
583
- )
584
- const postAmount = new _bn.default(
585
- _lodash.default.get(postBalance, 'uiTokenAmount.amount', '0'),
586
- 10
587
- )
588
- const amount = postAmount.sub(preAmount)
589
- if (!tokenAccount || amount.isZero()) return null // This is not perfect as there could be multiple same-token transfers in single
590
- // transaction, but our wallet only supports one transaction with single txId
591
- // so we are picking first that matches (correct token + type - send or receive)
592
-
593
- const match = innerInstructions.find((inner) => {
594
- const targetOwner = amount.isNeg() ? ownerAddress : null
595
- return (
596
- inner.token.mintAddress === tokenAccount.mintAddress && targetOwner === inner.owner
597
- )
598
- }) // It's possible we won't find a match, because our innerInstructions only contain
599
- // spl-token transfers, but balances of SPL tokens can change in different ways too.
600
- // for now, we are ignoring this to simplify as those cases are not that common, but
601
- // they should be handled eventually. It was already a scretch to add unparsed txs logic
602
- // to existing parser, expanding it further is not going to end well.
603
- // this probably should be refactored from ground to handle all those transactions
604
- // as a core part of it in the future
605
-
606
- if (!match) return null
607
- const { from, to, owner } = match
608
- return {
609
- id: txDetails.transaction.signatures[0],
610
- slot: txDetails.slot,
611
- owner,
612
- from,
613
- to,
614
- amount: amount.abs().toString(),
615
- // inconsistent with the rest, but it can and did overflow
616
- fee: 0,
617
- token: tokenAccount,
618
- data: {
619
- inner: true,
620
- },
621
- }
622
- })
623
- .filter((ix) => !!ix)
624
- }
625
-
626
- instructions = instructions
627
- .filter((ix) => ix.parsed) // only known instructions
628
- .map((ix) => ({
629
- program: ix.program,
630
- // system or spl-token
631
- type: ix.parsed.type,
632
- // transfer, createAccount, initializeAccount
633
- ...ix.parsed.info,
634
- }))
635
- innerInstructions = innerInstructions
636
- .reduce((acc, val) => {
637
- return acc.concat(val.instructions)
638
- }, [])
639
- .map((ix) => {
640
- const type = _lodash.default.get(ix, 'parsed.type')
641
-
642
- const isTransferTx = ix.parsed && ix.program === 'spl-token' && type === 'transfer'
643
-
644
- const source = _lodash.default.get(ix, 'parsed.info.source')
645
-
646
- const destination = _lodash.default.get(ix, 'parsed.info.destination')
647
-
648
- const amount = Number(_lodash.default.get(ix, 'parsed.info.amount', 0))
649
- const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
650
- return [source, destination].includes(tokenAccountAddress)
651
- })
652
- const isSending = !!tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
653
- return [source].includes(tokenAccountAddress)
654
- }) // owner if it's a send tx
655
-
656
- const instruction = {
657
- id: txDetails.transaction.signatures[0],
658
- slot: txDetails.slot,
659
- owner: isSending ? ownerAddress : null,
660
- from: source,
661
- to: destination,
662
- amount,
663
- token: tokenAccount,
664
- fee: isSending ? fee : 0,
665
- }
666
- return isTransferTx && tokenAccount ? instruction : null
667
- })
668
- .filter((ix) => !!ix) // program:type tells us if it's a SOL or Token transfer
669
-
670
- const solanaTx = _lodash.default.find(instructions, (ix) => {
671
- if (![ix.source, ix.destination].includes(ownerAddress)) return false
672
- return ix.program === 'system' && ix.type === 'transfer'
673
- }) // get SOL transfer
674
-
675
- const stakeTx = _lodash.default.find(instructions, {
676
- program: 'system',
677
- type: 'createAccountWithSeed',
678
- })
679
-
680
- const stakeWithdraw = _lodash.default.find(instructions, {
681
- program: 'stake',
682
- type: 'withdraw',
683
- })
684
-
685
- const stakeUndelegate = _lodash.default.find(instructions, {
686
- program: 'stake',
687
- type: 'deactivate',
688
- })
689
-
690
- const hasSolanaTx = solanaTx && !preTokenBalances.length && !postTokenBalances.length // only SOL moved and no tokens movements
691
-
692
- let tx = {}
693
-
694
- if (hasSolanaTx) {
695
- // Solana tx
696
- const isSending = ownerAddress === solanaTx.source
697
- tx = {
698
- owner: solanaTx.source,
699
- from: solanaTx.source,
700
- to: solanaTx.destination,
701
- amount: solanaTx.lamports,
702
- // number
703
- fee: isSending ? fee : 0,
704
- }
705
- } else if (stakeTx) {
706
- // start staking
707
- tx = {
708
- owner: stakeTx.base,
709
- from: stakeTx.base,
710
- to: stakeTx.base,
711
- amount: stakeTx.lamports,
712
- fee,
713
- staking: {
714
- method: 'createAccountWithSeed',
715
- seed: stakeTx.seed,
716
- stakeAddresses: [stakeTx.newAccount],
717
- stake: stakeTx.lamports,
718
- },
719
- }
720
- } else if (stakeWithdraw) {
721
- const stakeAccounts = _lodash.default.map(
722
- _lodash.default.filter(instructions, {
723
- program: 'stake',
724
- type: 'withdraw',
725
- }),
726
- 'stakeAccount'
727
- )
728
-
729
- tx = {
730
- owner: stakeWithdraw.withdrawAuthority,
731
- from: stakeWithdraw.stakeAccount,
732
- to: stakeWithdraw.destination,
733
- amount: stakeWithdraw.lamports,
734
- fee,
735
- staking: {
736
- method: 'withdraw',
737
- stakeAddresses: stakeAccounts,
738
- stake: stakeWithdraw.lamports,
739
- },
740
- }
741
- } else if (stakeUndelegate) {
742
- const stakeAccounts = _lodash.default.map(
743
- _lodash.default.filter(instructions, {
744
- program: 'stake',
745
- type: 'deactivate',
746
- }),
747
- 'stakeAccount'
748
- )
749
-
750
- tx = {
751
- owner: stakeUndelegate.stakeAuthority,
752
- from: stakeUndelegate.stakeAuthority,
753
- to: stakeUndelegate.stakeAccount,
754
- // obsolete
755
- amount: 0,
756
- fee,
757
- staking: {
758
- method: 'undelegate',
759
- stakeAddresses: stakeAccounts,
760
- },
761
- }
762
- } else {
763
- // Token tx
764
- _assert.default.ok(
765
- Array.isArray(tokenAccountsByOwner),
766
- 'tokenAccountsByOwner is required when parsing token tx'
767
- )
768
-
769
- let tokenTxs = _lodash.default
770
- .filter(instructions, ({ program, type }) => {
771
- return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
772
- }) // get Token transfer: could have more than 1 instructions
773
- .map((ix) => {
774
- // add token details based on source/destination address
775
- let tokenAccount = _lodash.default.find(tokenAccountsByOwner, {
776
- tokenAccountAddress: ix.source,
777
- })
778
-
779
- const isSending = !!tokenAccount
780
- if (!isSending)
781
- tokenAccount = _lodash.default.find(tokenAccountsByOwner, {
782
- tokenAccountAddress: ix.destination,
783
- }) // receiving
784
-
785
- if (!tokenAccount) return null // no transfers with our addresses involved
786
-
787
- const owner = isSending ? ownerAddress : null
788
- delete tokenAccount.balance
789
- delete tokenAccount.owner
790
- return {
791
- owner,
792
- token: tokenAccount,
793
- from: ix.source,
794
- to: ix.destination,
795
- amount: Number(ix.amount || _lodash.default.get(ix, 'tokenAmount.amount', 0)),
796
- // supporting both types: transfer and transferChecked
797
- fee: isSending ? fee : 0, // in lamports
798
- }
799
- })
800
-
801
- if (tokenTxs.length) {
802
- // found spl-token simple transfer/transferChecked instruction
803
- // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
804
- tx = tokenTxs.reduce((finalTx, ix) => {
805
- if (!ix) return finalTx // skip null instructions
806
-
807
- if (!finalTx.token) return ix // init finalTx (support just 1 token type per tx)
808
-
809
- if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount
810
- return finalTx
811
- }, {})
812
- } else if (preTokenBalances && postTokenBalances) {
813
- // probably a DEX program is involved (multiple instructions), compute balance changes
814
- const accountIndexes = _lodash.default.mapKeys(accountKeys, (x, i) => i)
815
-
816
- Object.values(accountIndexes).forEach((acc) => {
817
- // filter by ownerAddress
818
- const hasKnownOwner = !!_lodash.default.find(tokenAccountsByOwner, {
819
- tokenAccountAddress: acc.pubkey,
820
- })
821
- acc.owner = hasKnownOwner ? ownerAddress : null
822
- }) // group by owner and supported token
823
-
824
- const preBalances = preTokenBalances.filter((t) => {
825
- return (
826
- accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
827
- )
828
- })
829
- const postBalances = postTokenBalances.filter((t) => {
830
- return (
831
- accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
832
- )
833
- })
834
-
835
- if (preBalances.length || postBalances.length) {
836
- tx = {}
837
-
838
- if (includeUnparsed && innerInstructions.length) {
839
- // when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
840
- // 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
841
- // 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
842
- // SOL->SPL swaps on Raydium and Orca.
843
- tx = getUnparsedTx(tx)
844
- tx.dexTxs = getInnerTxsFromBalanceChanges()
845
- } else {
846
- if (solanaTx) {
847
- // the base tx will be the one that moved solana.
848
- tx = {
849
- owner: solanaTx.source,
850
- from: solanaTx.source,
851
- to: solanaTx.destination,
852
- amount: solanaTx.lamports,
853
- // number
854
- fee: ownerAddress === solanaTx.source ? fee : 0,
855
- }
856
- } // If it has inner instructions then it's a DEX tx that moved SPL -> SPL
857
-
858
- if (innerInstructions.length) {
859
- tx.dexTxs = innerInstructions // if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
860
-
861
- if (!tx.from && !solanaTx) {
862
- tx = tx.dexTxs[0]
863
- tx.dexTxs = innerInstructions.slice(1)
864
- }
865
- }
866
- }
867
- }
868
- }
869
- }
870
-
871
- const unparsed = Object.keys(tx).length === 0
872
-
873
- if (unparsed && includeUnparsed) {
874
- tx = getUnparsedTx(tx)
875
- } // How tokens tx are parsed:
876
- // 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
877
- // 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
878
- // 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
879
- // QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
880
-
881
- return {
882
- id: txDetails.transaction.signatures[0],
883
- slot: txDetails.slot,
884
- error: !(txDetails.meta.err === null),
885
- ...tx,
886
- }
887
- }
888
-
889
- async getSupply(mintAddress) {
890
- const result = await this.rpcCall('getTokenSupply', [mintAddress])
891
- return _lodash.default.get(result, 'value.amount')
892
- }
893
-
894
- async getWalletTokensList({ tokenAccounts }) {
895
- const tokensMint = []
896
-
897
- for (let account of tokenAccounts) {
898
- const mint = account.mintAddress // skip cached NFT
899
-
900
- if (this.tokensToSkip[mint]) continue // skip 0 balance
901
-
902
- if (account.balance === '0') continue // skip NFT
903
-
904
- const supply = await this.getSupply(mint)
905
-
906
- if (supply === '1') {
907
- this.tokensToSkip[mint] = true
908
- continue
909
- } // OK
910
-
911
- tokensMint.push(mint)
912
- }
913
-
914
- return tokensMint
915
- }
916
-
917
- async getTokenAccountsByOwner(address, tokenTicker) {
918
- const { value: accountsList } = await this.rpcCall(
919
- 'getTokenAccountsByOwner',
920
- [
921
- address,
922
- {
923
- programId: _solanaLib.TOKEN_PROGRAM_ID.toBase58(),
924
- },
925
- {
926
- encoding: 'jsonParsed',
927
- },
928
- ],
929
- {
930
- address,
931
- }
932
- )
933
- const tokenAccounts = []
934
-
935
- for (let entry of accountsList) {
936
- const { pubkey, account } = entry
937
-
938
- const mint = _lodash.default.get(account, 'data.parsed.info.mint')
939
-
940
- const token = this.tokens[mint] || {
941
- name: 'unknown',
942
- ticker: 'UNKNOWN',
943
- }
944
-
945
- const balance = _lodash.default.get(account, 'data.parsed.info.tokenAmount.amount', '0')
946
-
947
- tokenAccounts.push({
948
- tokenAccountAddress: pubkey,
949
- owner: address,
950
- tokenName: token.name,
951
- ticker: token.ticker,
952
- balance,
953
- mintAddress: mint,
954
- })
955
- } // eventually filter by token
956
-
957
- return tokenTicker
958
- ? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
959
- : tokenAccounts
960
- }
961
-
962
- async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
963
- let accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
964
- const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
965
- if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
966
- return acc // filter by supported tokens only
967
-
968
- if (!acc[tokenName]) acc[tokenName] = Number(balance)
969
- // e.g { 'serum': 123 }
970
- else acc[tokenName] += Number(balance) // merge same token account balance
971
-
972
- return acc
973
- }, {})
974
- return tokensBalance
975
- }
976
-
977
- async isAssociatedTokenAccountActive(tokenAddress) {
978
- // Returns the token balance of an SPL Token account.
979
- try {
980
- await this.rpcCall('getTokenAccountBalance', [tokenAddress])
981
- return true
982
- } catch (e) {
983
- return false
984
- }
985
- } // Returns account balance of a SPL Token account.
986
-
987
- async getTokenBalance(tokenAddress) {
988
- const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
989
- return _lodash.default.get(result, 'value.amount')
990
- }
991
-
992
- async getAccountInfo(address, encoding = 'jsonParsed') {
993
- const { value } = await this.rpcCall(
994
- 'getAccountInfo',
995
- [
996
- address,
997
- {
998
- encoding,
999
- commitment: 'single',
1000
- },
1001
- ],
1002
- {
1003
- address,
1004
- }
1005
- )
1006
- return value
1007
- }
1008
-
1009
- async isSpl(address) {
1010
- const { owner } = await this.getAccountInfo(address)
1011
- return owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
1012
- }
1013
-
1014
- async getMetaplexMetadata(tokenMintAddress) {
1015
- const metaplexPDA = (0, _solanaLib.getMetadataAccount)(tokenMintAddress)
1016
- const res = await this.getAccountInfo(metaplexPDA, 'base64')
1017
-
1018
- const data = _lodash.default.get(res, 'data[0]')
1019
-
1020
- if (!data) return null
1021
- return (0, _solanaLib.deserializeMetaplexMetadata)(Buffer.from(data, 'base64'))
1022
- }
1023
-
1024
- async getDecimals(tokenMintAddress) {
1025
- const result = await this.rpcCall('getTokenSupply', [tokenMintAddress])
1026
- return _lodash.default.get(result, 'value.decimals', null)
1027
- }
1028
-
1029
- async getAddressType(address) {
1030
- // solana, token or null (unknown), meaning address has never been initialized
1031
- const value = await this.getAccountInfo(address)
1032
- if (value === null) return null
1033
- const account = {
1034
- executable: value.executable,
1035
- owner: value.owner,
1036
- lamports: value.lamports,
1037
- }
1038
- return account.owner === _solanaLib.SYSTEM_PROGRAM_ID.toBase58()
1039
- ? 'solana'
1040
- : account.owner === _solanaLib.TOKEN_PROGRAM_ID.toBase58()
1041
- ? 'token'
1042
- : null
1043
- }
1044
-
1045
- async getTokenAddressOwner(address) {
1046
- const value = await this.getAccountInfo(address)
1047
-
1048
- const owner = _lodash.default.get(value, 'data.parsed.info.owner', null)
1049
-
1050
- return owner
1051
- }
1052
-
1053
- async getAddressMint(address) {
1054
- const value = await this.getAccountInfo(address)
1055
-
1056
- const mintAddress = _lodash.default.get(value, 'data.parsed.info.mint', null) // token mint
1057
-
1058
- return mintAddress
1059
- }
1060
-
1061
- async isTokenAddress(address) {
1062
- const type = await this.getAddressType(address)
1063
- return type === 'token'
1064
- }
1065
-
1066
- async isSOLaddress(address) {
1067
- const type = await this.getAddressType(address)
1068
- return type === 'solana'
1069
- }
1070
-
1071
- async getStakeAccountsInfo(address) {
1072
- const params = [
1073
- _solanaLib.STAKE_PROGRAM_ID.toBase58(),
1074
- {
1075
- filters: [
1076
- {
1077
- memcmp: {
1078
- offset: 12,
1079
- bytes: address,
1080
- },
1081
- },
1082
- ],
1083
- encoding: 'jsonParsed',
1084
- },
1085
- ]
1086
- const res = await this.rpcCall('getProgramAccounts', params, {
1087
- address,
1088
- })
1089
- const accounts = {}
1090
- let totalStake = 0
1091
- let locked = 0
1092
- let withdrawable = 0
1093
- let pending = 0
1094
-
1095
- for (let entry of res) {
1096
- const addr = entry.pubkey
1097
-
1098
- const lamports = _lodash.default.get(entry, 'account.lamports', 0)
1099
-
1100
- const delegation = _lodash.default.get(entry, 'account.data.parsed.info.stake.delegation', {}) // could have no delegation if the created stake address did not perform a delegate transaction
1101
-
1102
- accounts[addr] = delegation
1103
- accounts[addr].lamports = lamports // sol balance
1104
-
1105
- accounts[addr].activationEpoch = Number(accounts[addr].activationEpoch) || 0
1106
- accounts[addr].deactivationEpoch = Number(accounts[addr].deactivationEpoch) || 0
1107
- let state = 'inactive'
1108
- if (delegation.activationEpoch) state = await this.getStakeActivation(addr)
1109
- accounts[addr].state = state
1110
- accounts[addr].isDeactivating = state === 'deactivating'
1111
- accounts[addr].canWithdraw = state === 'inactive'
1112
- accounts[addr].stake = Number(accounts[addr].stake) || 0 // active staked amount
1113
-
1114
- totalStake += accounts[addr].stake
1115
- locked += ['active', 'activating'].includes(accounts[addr].state) ? lamports : 0
1116
- withdrawable += accounts[addr].canWithdraw ? lamports : 0
1117
- pending += accounts[addr].isDeactivating ? lamports : 0
1118
- }
1119
-
1120
- return {
1121
- accounts,
1122
- totalStake,
1123
- locked,
1124
- withdrawable,
1125
- pending,
1126
- }
1127
- }
1128
-
1129
- async getRewards(stakingAddresses = []) {
1130
- if (!stakingAddresses.length) return 0 // custom endpoint!
1131
-
1132
- const rewards = await this.request(`rewards?addresses=${stakingAddresses.join(',')}`)
1133
- .get()
1134
- .error(500, () => ({})) // addresses not found
1135
- .error(400, () => ({}))
1136
- .json() // sum rewards for all addresses
1137
-
1138
- const earnings = Object.values(rewards).reduce((total, x) => {
1139
- return total + x
1140
- }, 0)
1141
- return earnings
1142
- }
1143
-
1144
- async getMinimumBalanceForRentExemption(size) {
1145
- return this.rpcCall('getMinimumBalanceForRentExemption', [size])
1146
- }
1147
-
1148
- async getProgramAccounts(programId, config) {
1149
- return this.rpcCall('getProgramAccounts', [programId, config])
1150
- }
1151
-
1152
- async getMultipleAccounts(pubkeys, config) {
1153
- const response = await this.rpcCall('getMultipleAccounts', [pubkeys, config])
1154
- return response && response.value ? response.value : []
1155
- }
1156
- /**
1157
- * Broadcast a signed transaction
1158
- */
1159
- }
1160
-
1161
- exports.Api = Api