@exodus/solana-api 2.5.3 → 2.5.4

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