@exodus/solana-api 3.13.0 → 3.13.2

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,32 @@
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.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.13.1...@exodus/solana-api@3.13.2) (2025-01-09)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: solana missing txId of dexTxs (#4818)
13
+
14
+
15
+ ### License
16
+
17
+
18
+ * license: re-license under MIT license (#4814)
19
+
20
+
21
+
22
+ ## [3.13.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.13.0...@exodus/solana-api@3.13.1) (2025-01-09)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+
28
+ * fix: missing token transaction when parsing solana transactions (#4788)
29
+
30
+
31
+
6
32
  ## [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
33
 
8
34
 
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 Exodus Movement, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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.2",
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",
@@ -12,7 +12,7 @@
12
12
  "!src/**/__tests__"
13
13
  ],
14
14
  "author": "Exodus Movement, Inc.",
15
- "license": "ISC",
15
+ "license": "MIT",
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
@@ -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": "151e1c1fe2be1bce04f4a4717dbaf7589e172a1e",
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)
@@ -297,6 +311,7 @@ export class Api {
297
311
  innerInstructions = innerInstructions || []
298
312
 
299
313
  let { instructions, accountKeys } = txDetails.transaction.message
314
+ const txId = txDetails.transaction.signatures[0]
300
315
 
301
316
  const getUnparsedTx = () => {
302
317
  const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
@@ -360,7 +375,7 @@ export class Api {
360
375
  const { from, to, owner } = match
361
376
 
362
377
  return {
363
- id: txDetails.transaction.signatures[0],
378
+ id: txId,
364
379
  slot: txDetails.slot,
365
380
  owner,
366
381
  from,
@@ -401,30 +416,22 @@ export class Api {
401
416
  .reduce((acc, val) => {
402
417
  return [...acc, ...val.instructions]
403
418
  }, [])
419
+ .filter(
420
+ (ix) => ix.parsed && isSplTransferInstruction({ program: ix.program, type: ix.parsed.type })
421
+ )
404
422
  .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
423
  const source = lodash.get(ix, 'parsed.info.source')
414
424
  const destination = lodash.get(ix, 'parsed.info.destination')
415
425
  const amount = Number(
416
426
  lodash.get(ix, 'parsed.info.amount', 0) ||
417
427
  lodash.get(ix, 'parsed.info.tokenAmount.amount', 0)
418
428
  )
419
- const txId = txDetails.transaction.signatures[0]
420
- if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
421
- const getOwnerOrAccount = (account) =>
422
- account && account === accountToRedeemToOwner ? ownerAddress : account
429
+ const authority = lodash.get(ix, 'parsed.info.authority')
423
430
 
431
+ if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
424
432
  solanaTransferTx = {
425
- id: txId,
426
- from: getOwnerOrAccount(source),
427
- to: getOwnerOrAccount(destination),
433
+ from: authority || source,
434
+ to: ownerAddress,
428
435
  amount,
429
436
  fee,
430
437
  }
@@ -441,17 +448,16 @@ export class Api {
441
448
  })
442
449
 
443
450
  // owner if it's a send tx
444
- const instruction = {
451
+ return {
445
452
  id: txId,
446
453
  slot: txDetails.slot,
447
454
  owner: isSending ? ownerAddress : null,
448
- from: source,
449
- to: destination,
455
+ from: isSending ? ownerAddress : source,
456
+ to: isSending ? destination : ownerAddress,
450
457
  amount,
451
458
  token: tokenAccount,
452
459
  fee: isSending ? fee : 0,
453
460
  }
454
- return tokenAccount ? instruction : null
455
461
  })
456
462
  .filter((ix) => !!ix)
457
463
 
@@ -460,21 +466,9 @@ export class Api {
460
466
  const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
461
467
  const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
462
468
  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
469
 
466
470
  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) {
471
+ if (stakeTx) {
478
472
  // start staking
479
473
  tx = {
480
474
  owner: stakeTx.base,
@@ -523,64 +517,49 @@ export class Api {
523
517
  },
524
518
  }
525
519
  } else {
526
- // Token tx
527
- assert(
528
- Array.isArray(tokenAccountsByOwner),
529
- 'tokenAccountsByOwner is required when parsing token tx'
530
- )
520
+ if (solanaTransferTx) {
521
+ const isSending = ownerAddress === solanaTransferTx.source
522
+ tx = {
523
+ owner: solanaTransferTx.source,
524
+ from: solanaTransferTx.source,
525
+ to: solanaTransferTx.destination,
526
+ amount: solanaTransferTx.lamports, // number
527
+ fee: isSending ? fee : 0,
528
+ }
529
+ }
531
530
 
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
- })
531
+ // Parse Token txs
532
+ const tokenTxs = this._parseTokenTransfers({
533
+ instructions,
534
+ tokenAccountsByOwner,
535
+ ownerAddress,
536
+ fee,
537
+ preTokenBalances,
538
+ postTokenBalances,
539
+ })
561
540
 
562
541
  if (tokenTxs.length > 0) {
563
542
  // 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
- }, {})
543
+ tx.tokenTxs = tokenTxs.map((tx) => ({
544
+ id: txId,
545
+ slot: txDetails.slot,
546
+ ...tx,
547
+ }))
571
548
  } else if (preTokenBalances && postTokenBalances) {
572
549
  // probably a DEX program is involved (multiple instructions), compute balance changes
573
550
 
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
- })
551
+ const accountIndexes = accountKeys.reduce((acc, key, i) => {
552
+ const hasKnownOwner = tokenAccountsByOwner.some(
553
+ (tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
554
+ )
555
+
556
+ acc[i] = {
557
+ ...key,
558
+ owner: hasKnownOwner ? ownerAddress : null,
559
+ }
560
+
561
+ return acc
562
+ }, Object.create(null))
584
563
 
585
564
  // group by owner and supported token
586
565
  const preBalances = preTokenBalances.filter((t) => {
@@ -653,6 +632,54 @@ export class Api {
653
632
  }
654
633
  }
655
634
 
635
+ _parseTokenTransfers({
636
+ instructions,
637
+ tokenAccountsByOwner,
638
+ ownerAddress,
639
+ fee,
640
+ preTokenBalances,
641
+ postTokenBalances,
642
+ }) {
643
+ if (
644
+ preTokenBalances.length === 0 &&
645
+ postTokenBalances.length === 0 &&
646
+ !Array.isArray(tokenAccountsByOwner)
647
+ )
648
+ return []
649
+
650
+ const tokenTxs = []
651
+
652
+ instructions.forEach((instruction) => {
653
+ const { type, program, source, destination, amount, tokenAmount } = instruction
654
+
655
+ if (isSplTransferInstruction({ program, type })) {
656
+ let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: source })
657
+ const isSending = !!tokenAccount
658
+ if (!isSending)
659
+ tokenAccount = lodash.find(tokenAccountsByOwner, {
660
+ tokenAccountAddress: destination,
661
+ }) // receiving
662
+ if (!tokenAccount) return // no transfers with our addresses involved
663
+
664
+ const owner = isSending ? ownerAddress : null
665
+
666
+ delete tokenAccount.balance
667
+ delete tokenAccount.owner
668
+
669
+ tokenTxs.push({
670
+ owner,
671
+ token: tokenAccount,
672
+ from: isSending ? ownerAddress : source,
673
+ to: isSending ? destination : ownerAddress,
674
+ amount: Number(amount || tokenAmount?.amount || 0), // supporting types: transfer, transferChecked, transferCheckedWithFee
675
+ fee: isSending ? fee : 0, // in lamports
676
+ })
677
+ }
678
+ })
679
+
680
+ return tokenTxs
681
+ }
682
+
656
683
  async getWalletTokensList({ tokenAccounts }) {
657
684
  const tokensMint = []
658
685
  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)