@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 +10 -0
- package/package.json +2 -2
- package/src/api.js +116 -92
- package/src/txs-utils.js +9 -0
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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 =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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)
|