@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 +26 -0
- package/LICENSE +7 -0
- package/package.json +3 -3
- package/src/api.js +118 -91
- package/src/txs-utils.js +9 -0
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.
|
|
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": "
|
|
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": "
|
|
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
|
-
|
|
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)
|
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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 =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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)
|