@exodus/solana-api 3.13.0 → 3.13.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/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.13.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.13.0...@exodus/solana-api@3.13.1) (2025-01-09)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: missing token transaction when parsing solana transactions (#4788)
13
+
14
+
15
+
6
16
  ## [3.13.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.12.1...@exodus/solana-api@3.13.0) (2025-01-08)
7
17
 
8
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.13.0",
3
+ "version": "3.13.1",
4
4
  "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Solana",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -47,7 +47,7 @@
47
47
  "@exodus/assets-testing": "^1.0.0",
48
48
  "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
49
49
  },
50
- "gitHead": "aac8ba83fb14935634067e0870c88939afa92b00",
50
+ "gitHead": "8593d378a2c8054dea5d94dac3bf96b124842903",
51
51
  "bugs": {
52
52
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
53
53
  },
package/src/api.js CHANGED
@@ -16,13 +16,13 @@ import {
16
16
  } from '@exodus/solana-lib'
17
17
  import BN from 'bn.js'
18
18
  import lodash from 'lodash'
19
- import assert from 'minimalistic-assert'
20
19
  import ms from 'ms'
21
20
  import urljoin from 'url-join'
22
21
  import wretch from 'wretch'
23
22
 
24
23
  import { Connection } from './connection.js'
25
24
  import { getStakeActivation } from './get-stake-activation/index.js'
25
+ import { isSplTransferInstruction } from './txs-utils.js'
26
26
 
27
27
  const createApi = createApiCJS.default || createApiCJS
28
28
 
@@ -250,7 +250,8 @@ export class Api {
250
250
  const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner, {
251
251
  includeUnparsed,
252
252
  })
253
- if (!parsedTx.from && !includeUnparsed) return // cannot parse it
253
+
254
+ if (!parsedTx.from && parsedTx.tokenTxs?.length === 0 && !includeUnparsed) return // cannot parse it
254
255
 
255
256
  // split dexTx in separate txs
256
257
  if (parsedTx.dexTxs) {
@@ -264,11 +265,24 @@ export class Api {
264
265
  delete parsedTx.dexTxs
265
266
  }
266
267
 
267
- transactions.push({
268
- timestamp,
269
- date: new Date(timestamp),
270
- ...parsedTx,
271
- })
268
+ if (parsedTx.tokenTxs?.length > 0) {
269
+ parsedTx.tokenTxs.forEach((tx) => {
270
+ transactions.push({
271
+ timestamp,
272
+ date: new Date(timestamp),
273
+ ...tx,
274
+ })
275
+ })
276
+ delete parsedTx.tokenTxs
277
+ }
278
+
279
+ if (parsedTx.from) {
280
+ transactions.push({
281
+ timestamp,
282
+ date: new Date(timestamp),
283
+ ...parsedTx,
284
+ })
285
+ }
272
286
  })
273
287
  } catch (err) {
274
288
  console.warn('Solana error:', err)
@@ -401,30 +415,22 @@ export class Api {
401
415
  .reduce((acc, val) => {
402
416
  return [...acc, ...val.instructions]
403
417
  }, [])
418
+ .filter(
419
+ (ix) => ix.parsed && isSplTransferInstruction({ program: ix.program, type: ix.parsed.type })
420
+ )
404
421
  .map((ix) => {
405
- const type = lodash.get(ix, 'parsed.type')
406
- const isTransferTx =
407
- ix.parsed &&
408
- ix.program === 'spl-token' &&
409
- ['transfer', 'transferChecked', 'transferCheckedWithFee'].includes(type)
410
-
411
- if (!isTransferTx) return null
412
-
413
422
  const source = lodash.get(ix, 'parsed.info.source')
414
423
  const destination = lodash.get(ix, 'parsed.info.destination')
415
424
  const amount = Number(
416
425
  lodash.get(ix, 'parsed.info.amount', 0) ||
417
426
  lodash.get(ix, 'parsed.info.tokenAmount.amount', 0)
418
427
  )
419
- const txId = txDetails.transaction.signatures[0]
420
- if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
421
- const getOwnerOrAccount = (account) =>
422
- account && account === accountToRedeemToOwner ? ownerAddress : account
428
+ const authority = lodash.get(ix, 'parsed.info.authority')
423
429
 
430
+ if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
424
431
  solanaTransferTx = {
425
- id: txId,
426
- from: getOwnerOrAccount(source),
427
- to: getOwnerOrAccount(destination),
432
+ from: authority || source,
433
+ to: ownerAddress,
428
434
  amount,
429
435
  fee,
430
436
  }
@@ -441,17 +447,14 @@ export class Api {
441
447
  })
442
448
 
443
449
  // owner if it's a send tx
444
- const instruction = {
445
- id: txId,
446
- slot: txDetails.slot,
450
+ return {
447
451
  owner: isSending ? ownerAddress : null,
448
- from: source,
449
- to: destination,
452
+ from: isSending ? ownerAddress : source,
453
+ to: isSending ? destination : ownerAddress,
450
454
  amount,
451
455
  token: tokenAccount,
452
456
  fee: isSending ? fee : 0,
453
457
  }
454
- return tokenAccount ? instruction : null
455
458
  })
456
459
  .filter((ix) => !!ix)
457
460
 
@@ -460,21 +463,9 @@ export class Api {
460
463
  const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
461
464
  const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
462
465
  const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
463
- const hasOnlySolanaTx =
464
- solanaTransferTx && preTokenBalances.length === 0 && postTokenBalances.length === 0 // only SOL moved and no tokens movements
465
466
 
466
467
  let tx = {}
467
- if (hasOnlySolanaTx) {
468
- // Solana tx
469
- const isSending = ownerAddress === solanaTransferTx.source
470
- tx = {
471
- owner: solanaTransferTx.source,
472
- from: solanaTransferTx.source,
473
- to: solanaTransferTx.destination,
474
- amount: solanaTransferTx.lamports, // number
475
- fee: isSending ? fee : 0,
476
- }
477
- } else if (stakeTx) {
468
+ if (stakeTx) {
478
469
  // start staking
479
470
  tx = {
480
471
  owner: stakeTx.base,
@@ -523,64 +514,49 @@ export class Api {
523
514
  },
524
515
  }
525
516
  } else {
526
- // Token tx
527
- assert(
528
- Array.isArray(tokenAccountsByOwner),
529
- 'tokenAccountsByOwner is required when parsing token tx'
530
- )
517
+ if (solanaTransferTx) {
518
+ const isSending = ownerAddress === solanaTransferTx.source
519
+ tx = {
520
+ owner: solanaTransferTx.source,
521
+ from: solanaTransferTx.source,
522
+ to: solanaTransferTx.destination,
523
+ amount: solanaTransferTx.lamports, // number
524
+ fee: isSending ? fee : 0,
525
+ }
526
+ }
531
527
 
532
- const tokenTxs = lodash
533
- .filter(instructions, ({ program, type }) => {
534
- return (
535
- program === 'spl-token' &&
536
- ['transfer', 'transferChecked', 'transferCheckedWithFee'].includes(type)
537
- )
538
- }) // get Token transfer: could have more than 1 instructions
539
- .map((ix) => {
540
- // add token details based on source/destination address
541
- let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: ix.source })
542
- const isSending = !!tokenAccount
543
- if (!isSending)
544
- tokenAccount = lodash.find(tokenAccountsByOwner, {
545
- tokenAccountAddress: ix.destination,
546
- }) // receiving
547
- if (!tokenAccount) return null // no transfers with our addresses involved
548
- const owner = isSending ? ownerAddress : null
549
-
550
- delete tokenAccount.balance
551
- delete tokenAccount.owner
552
- return {
553
- owner,
554
- token: tokenAccount,
555
- from: ix.source,
556
- to: ix.destination,
557
- amount: Number(ix.amount || lodash.get(ix, 'tokenAmount.amount', 0)), // supporting types: transfer, transferChecked, transferCheckedWithFee
558
- fee: isSending ? fee : 0, // in lamports
559
- }
560
- })
528
+ // Parse Token txs
529
+ const tokenTxs = this._parseTokenTransfers({
530
+ instructions,
531
+ tokenAccountsByOwner,
532
+ ownerAddress,
533
+ fee,
534
+ preTokenBalances,
535
+ postTokenBalances,
536
+ })
561
537
 
562
538
  if (tokenTxs.length > 0) {
563
539
  // found spl-token simple transfer/transferChecked instruction
564
- // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
565
- tx = tokenTxs.reduce((finalTx, ix) => {
566
- if (!ix) return finalTx // skip null instructions
567
- if (!finalTx.token) return ix // init finalTx (support just 1 token type per tx)
568
- if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount
569
- return finalTx
570
- }, {})
540
+ tx.tokenTxs = tokenTxs.map((tx) => ({
541
+ id: txDetails.transaction.signatures[0],
542
+ slot: txDetails.slot,
543
+ ...tx,
544
+ }))
571
545
  } else if (preTokenBalances && postTokenBalances) {
572
546
  // probably a DEX program is involved (multiple instructions), compute balance changes
573
547
 
574
- const accountIndexes = lodash.mapKeys(accountKeys, (x, i) => i)
575
- Object.values(accountIndexes).forEach((acc) => {
576
- // filter by ownerAddress
577
- // eslint-disable-next-line unicorn/prefer-array-some
578
- const hasKnownOwner = !!lodash.find(tokenAccountsByOwner, {
579
- tokenAccountAddress: acc.pubkey,
580
- })
581
- // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
582
- acc.owner = hasKnownOwner ? ownerAddress : null
583
- })
548
+ const accountIndexes = accountKeys.reduce((acc, key, i) => {
549
+ const hasKnownOwner = tokenAccountsByOwner.some(
550
+ (tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
551
+ )
552
+
553
+ acc[i] = {
554
+ ...key,
555
+ owner: hasKnownOwner ? ownerAddress : null,
556
+ }
557
+
558
+ return acc
559
+ }, Object.create(null))
584
560
 
585
561
  // group by owner and supported token
586
562
  const preBalances = preTokenBalances.filter((t) => {
@@ -653,6 +629,54 @@ export class Api {
653
629
  }
654
630
  }
655
631
 
632
+ _parseTokenTransfers({
633
+ instructions,
634
+ tokenAccountsByOwner,
635
+ ownerAddress,
636
+ fee,
637
+ preTokenBalances,
638
+ postTokenBalances,
639
+ }) {
640
+ if (
641
+ preTokenBalances.length === 0 &&
642
+ postTokenBalances.length === 0 &&
643
+ !Array.isArray(tokenAccountsByOwner)
644
+ )
645
+ return []
646
+
647
+ const tokenTxs = []
648
+
649
+ instructions.forEach((instruction) => {
650
+ const { type, program, source, destination, amount, tokenAmount } = instruction
651
+
652
+ if (isSplTransferInstruction({ program, type })) {
653
+ let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: source })
654
+ const isSending = !!tokenAccount
655
+ if (!isSending)
656
+ tokenAccount = lodash.find(tokenAccountsByOwner, {
657
+ tokenAccountAddress: destination,
658
+ }) // receiving
659
+ if (!tokenAccount) return // no transfers with our addresses involved
660
+
661
+ const owner = isSending ? ownerAddress : null
662
+
663
+ delete tokenAccount.balance
664
+ delete tokenAccount.owner
665
+
666
+ tokenTxs.push({
667
+ owner,
668
+ token: tokenAccount,
669
+ from: isSending ? ownerAddress : source,
670
+ to: isSending ? destination : ownerAddress,
671
+ amount: Number(amount || tokenAmount?.amount || 0), // supporting types: transfer, transferChecked, transferCheckedWithFee
672
+ fee: isSending ? fee : 0, // in lamports
673
+ })
674
+ }
675
+ })
676
+
677
+ return tokenTxs
678
+ }
679
+
656
680
  async getWalletTokensList({ tokenAccounts }) {
657
681
  const tokensMint = []
658
682
  for (const account of tokenAccounts) {
package/src/txs-utils.js CHANGED
@@ -1,3 +1,9 @@
1
+ const TRANSFER_INSTRUCTION_TYPES = new Set([
2
+ 'transfer',
3
+ 'transferChecked',
4
+ 'transferCheckedWithFee',
5
+ ])
6
+
1
7
  const isSolanaTx = (tx) => tx.coinName === 'solana'
2
8
  export const isSolanaStaking = (tx) =>
3
9
  isSolanaTx(tx) && ['createAccountWithSeed', 'delegate'].includes(tx?.data?.staking?.method)
@@ -6,3 +12,6 @@ export const isSolanaUnstaking = (tx) =>
6
12
  export const isSolanaWithdrawn = (tx) => isSolanaTx(tx) && tx?.data?.staking?.method === 'withdraw'
7
13
  export const isSolanaRewardsActivityTx = (tx) =>
8
14
  [isSolanaStaking, isSolanaUnstaking, isSolanaWithdrawn].some((fn) => fn(tx))
15
+
16
+ export const isSplTransferInstruction = ({ program, type }) =>
17
+ program === 'spl-token' && TRANSFER_INSTRUCTION_TYPES.has(type)