@exodus/solana-api 3.12.1 → 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 +20 -0
- package/package.json +2 -2
- package/src/api.js +142 -92
- package/src/txs-utils.js +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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
|
+
|
|
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)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat: add SOL waitForTransactionStatus method (#4800)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [3.12.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.12.0...@exodus/solana-api@3.12.1) (2025-01-02)
|
|
7
27
|
|
|
8
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.
|
|
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
|
|
|
@@ -178,6 +178,32 @@ export class Api {
|
|
|
178
178
|
return this.rpcCall('getBlockTime', [slot])
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
async waitForTransactionStatus(txIds, status = 'finalized', timeoutMs = ms('1m')) {
|
|
182
|
+
if (!Array.isArray(txIds)) txIds = [txIds]
|
|
183
|
+
const startTime = Date.now()
|
|
184
|
+
|
|
185
|
+
while (true) {
|
|
186
|
+
const response = await this.rpcCall('getSignatureStatuses', [
|
|
187
|
+
txIds,
|
|
188
|
+
{ searchTransactionHistory: true },
|
|
189
|
+
])
|
|
190
|
+
const data = response.value
|
|
191
|
+
const allTxsAreConfirmed = data.every((elem) => elem?.confirmationStatus === status)
|
|
192
|
+
if (allTxsAreConfirmed) {
|
|
193
|
+
return true
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if the timeout has elapsed
|
|
197
|
+
if (Date.now() - startTime >= timeoutMs) {
|
|
198
|
+
// timeout
|
|
199
|
+
throw new Error('waitForTransactionStatus timeout')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Wait for the specified interval before the next request
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, ms('10s')))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
181
207
|
async getSignaturesForAddress(address, { until, before, limit } = {}) {
|
|
182
208
|
until = until || undefined
|
|
183
209
|
return this.rpcCall('getSignaturesForAddress', [address, { until, before, limit }], { address })
|
|
@@ -224,7 +250,8 @@ export class Api {
|
|
|
224
250
|
const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner, {
|
|
225
251
|
includeUnparsed,
|
|
226
252
|
})
|
|
227
|
-
|
|
253
|
+
|
|
254
|
+
if (!parsedTx.from && parsedTx.tokenTxs?.length === 0 && !includeUnparsed) return // cannot parse it
|
|
228
255
|
|
|
229
256
|
// split dexTx in separate txs
|
|
230
257
|
if (parsedTx.dexTxs) {
|
|
@@ -238,11 +265,24 @@ export class Api {
|
|
|
238
265
|
delete parsedTx.dexTxs
|
|
239
266
|
}
|
|
240
267
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
286
|
})
|
|
247
287
|
} catch (err) {
|
|
248
288
|
console.warn('Solana error:', err)
|
|
@@ -375,30 +415,22 @@ export class Api {
|
|
|
375
415
|
.reduce((acc, val) => {
|
|
376
416
|
return [...acc, ...val.instructions]
|
|
377
417
|
}, [])
|
|
418
|
+
.filter(
|
|
419
|
+
(ix) => ix.parsed && isSplTransferInstruction({ program: ix.program, type: ix.parsed.type })
|
|
420
|
+
)
|
|
378
421
|
.map((ix) => {
|
|
379
|
-
const type = lodash.get(ix, 'parsed.type')
|
|
380
|
-
const isTransferTx =
|
|
381
|
-
ix.parsed &&
|
|
382
|
-
ix.program === 'spl-token' &&
|
|
383
|
-
['transfer', 'transferChecked', 'transferCheckedWithFee'].includes(type)
|
|
384
|
-
|
|
385
|
-
if (!isTransferTx) return null
|
|
386
|
-
|
|
387
422
|
const source = lodash.get(ix, 'parsed.info.source')
|
|
388
423
|
const destination = lodash.get(ix, 'parsed.info.destination')
|
|
389
424
|
const amount = Number(
|
|
390
425
|
lodash.get(ix, 'parsed.info.amount', 0) ||
|
|
391
426
|
lodash.get(ix, 'parsed.info.tokenAmount.amount', 0)
|
|
392
427
|
)
|
|
393
|
-
const
|
|
394
|
-
if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
|
|
395
|
-
const getOwnerOrAccount = (account) =>
|
|
396
|
-
account && account === accountToRedeemToOwner ? ownerAddress : account
|
|
428
|
+
const authority = lodash.get(ix, 'parsed.info.authority')
|
|
397
429
|
|
|
430
|
+
if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
|
|
398
431
|
solanaTransferTx = {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
to: getOwnerOrAccount(destination),
|
|
432
|
+
from: authority || source,
|
|
433
|
+
to: ownerAddress,
|
|
402
434
|
amount,
|
|
403
435
|
fee,
|
|
404
436
|
}
|
|
@@ -415,17 +447,14 @@ export class Api {
|
|
|
415
447
|
})
|
|
416
448
|
|
|
417
449
|
// owner if it's a send tx
|
|
418
|
-
|
|
419
|
-
id: txId,
|
|
420
|
-
slot: txDetails.slot,
|
|
450
|
+
return {
|
|
421
451
|
owner: isSending ? ownerAddress : null,
|
|
422
|
-
from: source,
|
|
423
|
-
to: destination,
|
|
452
|
+
from: isSending ? ownerAddress : source,
|
|
453
|
+
to: isSending ? destination : ownerAddress,
|
|
424
454
|
amount,
|
|
425
455
|
token: tokenAccount,
|
|
426
456
|
fee: isSending ? fee : 0,
|
|
427
457
|
}
|
|
428
|
-
return tokenAccount ? instruction : null
|
|
429
458
|
})
|
|
430
459
|
.filter((ix) => !!ix)
|
|
431
460
|
|
|
@@ -434,21 +463,9 @@ export class Api {
|
|
|
434
463
|
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
435
464
|
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
436
465
|
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
437
|
-
const hasOnlySolanaTx =
|
|
438
|
-
solanaTransferTx && preTokenBalances.length === 0 && postTokenBalances.length === 0 // only SOL moved and no tokens movements
|
|
439
466
|
|
|
440
467
|
let tx = {}
|
|
441
|
-
if (
|
|
442
|
-
// Solana tx
|
|
443
|
-
const isSending = ownerAddress === solanaTransferTx.source
|
|
444
|
-
tx = {
|
|
445
|
-
owner: solanaTransferTx.source,
|
|
446
|
-
from: solanaTransferTx.source,
|
|
447
|
-
to: solanaTransferTx.destination,
|
|
448
|
-
amount: solanaTransferTx.lamports, // number
|
|
449
|
-
fee: isSending ? fee : 0,
|
|
450
|
-
}
|
|
451
|
-
} else if (stakeTx) {
|
|
468
|
+
if (stakeTx) {
|
|
452
469
|
// start staking
|
|
453
470
|
tx = {
|
|
454
471
|
owner: stakeTx.base,
|
|
@@ -497,64 +514,49 @@ export class Api {
|
|
|
497
514
|
},
|
|
498
515
|
}
|
|
499
516
|
} else {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
+
}
|
|
505
527
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: ix.source })
|
|
516
|
-
const isSending = !!tokenAccount
|
|
517
|
-
if (!isSending)
|
|
518
|
-
tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
519
|
-
tokenAccountAddress: ix.destination,
|
|
520
|
-
}) // receiving
|
|
521
|
-
if (!tokenAccount) return null // no transfers with our addresses involved
|
|
522
|
-
const owner = isSending ? ownerAddress : null
|
|
523
|
-
|
|
524
|
-
delete tokenAccount.balance
|
|
525
|
-
delete tokenAccount.owner
|
|
526
|
-
return {
|
|
527
|
-
owner,
|
|
528
|
-
token: tokenAccount,
|
|
529
|
-
from: ix.source,
|
|
530
|
-
to: ix.destination,
|
|
531
|
-
amount: Number(ix.amount || lodash.get(ix, 'tokenAmount.amount', 0)), // supporting types: transfer, transferChecked, transferCheckedWithFee
|
|
532
|
-
fee: isSending ? fee : 0, // in lamports
|
|
533
|
-
}
|
|
534
|
-
})
|
|
528
|
+
// Parse Token txs
|
|
529
|
+
const tokenTxs = this._parseTokenTransfers({
|
|
530
|
+
instructions,
|
|
531
|
+
tokenAccountsByOwner,
|
|
532
|
+
ownerAddress,
|
|
533
|
+
fee,
|
|
534
|
+
preTokenBalances,
|
|
535
|
+
postTokenBalances,
|
|
536
|
+
})
|
|
535
537
|
|
|
536
538
|
if (tokenTxs.length > 0) {
|
|
537
539
|
// found spl-token simple transfer/transferChecked instruction
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
return finalTx
|
|
544
|
-
}, {})
|
|
540
|
+
tx.tokenTxs = tokenTxs.map((tx) => ({
|
|
541
|
+
id: txDetails.transaction.signatures[0],
|
|
542
|
+
slot: txDetails.slot,
|
|
543
|
+
...tx,
|
|
544
|
+
}))
|
|
545
545
|
} else if (preTokenBalances && postTokenBalances) {
|
|
546
546
|
// probably a DEX program is involved (multiple instructions), compute balance changes
|
|
547
547
|
|
|
548
|
-
const accountIndexes =
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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))
|
|
558
560
|
|
|
559
561
|
// group by owner and supported token
|
|
560
562
|
const preBalances = preTokenBalances.filter((t) => {
|
|
@@ -627,6 +629,54 @@ export class Api {
|
|
|
627
629
|
}
|
|
628
630
|
}
|
|
629
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
|
+
|
|
630
680
|
async getWalletTokensList({ tokenAccounts }) {
|
|
631
681
|
const tokensMint = []
|
|
632
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)
|