@exodus/solana-api 3.25.4 → 3.26.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 +30 -0
- package/package.json +3 -3
- package/src/api.js +27 -632
- package/src/connection.js +42 -111
- package/src/index.js +2 -0
- package/src/rpc-api.js +0 -8
- package/src/tx-log/README.md +63 -0
- package/src/tx-log/clarity-monitor.js +29 -13
- package/src/tx-log/index.js +3 -2
- package/src/tx-log/solana-monitor.js +25 -14
- package/src/tx-log/ws-monitor.js +390 -0
- package/src/tx-parser.js +533 -0
- package/src/ws-api.js +263 -0
package/src/api.js
CHANGED
|
@@ -20,22 +20,14 @@ import lodash from 'lodash'
|
|
|
20
20
|
import ms from 'ms'
|
|
21
21
|
import urljoin from 'url-join'
|
|
22
22
|
|
|
23
|
-
import { Connection } from './connection.js'
|
|
24
23
|
import { getStakeActivation } from './get-stake-activation/index.js'
|
|
25
|
-
import {
|
|
26
|
-
isSolTransferInstruction,
|
|
27
|
-
isSplMintInstruction,
|
|
28
|
-
isSplTransferInstruction,
|
|
29
|
-
} from './txs-utils.js'
|
|
24
|
+
import { parseTransaction } from './tx-parser.js'
|
|
30
25
|
|
|
31
26
|
const createApi = createApiCJS.default || createApiCJS
|
|
32
27
|
|
|
33
28
|
// Doc: https://docs.solana.com/apps/jsonrpc-api
|
|
34
29
|
|
|
35
30
|
const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com
|
|
36
|
-
const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // not standard across all node providers (we're compatible only with Quicknode)
|
|
37
|
-
const FORCE_HTTP = true // use https over ws
|
|
38
|
-
const ZERO = BigInt(0)
|
|
39
31
|
|
|
40
32
|
const errorMessagesToRetry = [
|
|
41
33
|
'Blockhash not found',
|
|
@@ -44,14 +36,11 @@ const errorMessagesToRetry = [
|
|
|
44
36
|
|
|
45
37
|
// Tokens + SOL api support
|
|
46
38
|
export class Api {
|
|
47
|
-
constructor({ rpcUrl, wsUrl, assets, txsLimit
|
|
48
|
-
this.tokenAssetType = tokenAssetType
|
|
39
|
+
constructor({ rpcUrl, wsUrl, assets, txsLimit }) {
|
|
49
40
|
this.setServer(rpcUrl)
|
|
50
|
-
this.setWsEndpoint(wsUrl)
|
|
51
41
|
this.setTokens(assets)
|
|
52
42
|
this.tokensToSkip = {}
|
|
53
43
|
this.txsLimit = txsLimit
|
|
54
|
-
this.connections = {}
|
|
55
44
|
this.getSupply = memoize(async (mintAddress) => {
|
|
56
45
|
// cached getSupply
|
|
57
46
|
const result = await this.rpcCall('getTokenSupply', [mintAddress])
|
|
@@ -76,12 +65,8 @@ export class Api {
|
|
|
76
65
|
this.api = createApi(this.rpcUrl)
|
|
77
66
|
}
|
|
78
67
|
|
|
79
|
-
setWsEndpoint(wsUrl) {
|
|
80
|
-
this.wsUrl = wsUrl || WS_ENDPOINT
|
|
81
|
-
}
|
|
82
|
-
|
|
83
68
|
setTokens(assets = {}) {
|
|
84
|
-
const solTokens = pickBy(assets, (asset) => asset.
|
|
69
|
+
const solTokens = pickBy(assets, (asset) => asset.name !== asset.baseAsset.name)
|
|
85
70
|
this.tokens = new Map(Object.values(solTokens).map((v) => [v.mintAddress, v]))
|
|
86
71
|
}
|
|
87
72
|
|
|
@@ -91,51 +76,7 @@ export class Api {
|
|
|
91
76
|
})
|
|
92
77
|
}
|
|
93
78
|
|
|
94
|
-
async
|
|
95
|
-
address,
|
|
96
|
-
tokensAddresses = [],
|
|
97
|
-
onMessage,
|
|
98
|
-
handleAccounts,
|
|
99
|
-
handleTransfers,
|
|
100
|
-
handleReconnect,
|
|
101
|
-
reconnectDelay,
|
|
102
|
-
}) {
|
|
103
|
-
if (this.connections[address]) return // already subscribed
|
|
104
|
-
const conn = new Connection({
|
|
105
|
-
endpoint: this.wsUrl,
|
|
106
|
-
address,
|
|
107
|
-
tokensAddresses,
|
|
108
|
-
onMsg: (json) => onMessage(json),
|
|
109
|
-
callback: (updates) =>
|
|
110
|
-
this.handleUpdates({ updates, address, handleAccounts, handleTransfers }),
|
|
111
|
-
reconnectCallback: handleReconnect,
|
|
112
|
-
reconnectDelay,
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
this.connections[address] = conn
|
|
116
|
-
return conn.start()
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async unwatchAddress({ address }) {
|
|
120
|
-
if (this.connections[address]) {
|
|
121
|
-
await this.connections[address].stop()
|
|
122
|
-
delete this.connections[address]
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async handleUpdates({ updates, address, handleAccounts, handleTransfers }) {
|
|
127
|
-
// console.log(`got ws updates from ${address}:`, updates)
|
|
128
|
-
if (handleTransfers) return handleTransfers(updates)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async rpcCall(method, params = [], { address = '', forceHttp = FORCE_HTTP } = {}) {
|
|
132
|
-
// ws request
|
|
133
|
-
const connection = this.connections[address] || lodash.sample(Object.values(this.connections)) // pick random connection
|
|
134
|
-
if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
|
|
135
|
-
return connection.sendMessage(method, params)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// http fallback
|
|
79
|
+
async rpcCall(method, params = []) {
|
|
139
80
|
return this.api.post({ method, params })
|
|
140
81
|
}
|
|
141
82
|
|
|
@@ -172,11 +113,9 @@ export class Api {
|
|
|
172
113
|
}
|
|
173
114
|
|
|
174
115
|
async getRecentBlockHash(commitment) {
|
|
175
|
-
const result = await this.rpcCall(
|
|
176
|
-
'
|
|
177
|
-
|
|
178
|
-
{ forceHttp: true }
|
|
179
|
-
)
|
|
116
|
+
const result = await this.rpcCall('getLatestBlockhash', [
|
|
117
|
+
{ commitment: commitment || 'confirmed', encoding: 'jsonParsed' },
|
|
118
|
+
])
|
|
180
119
|
return lodash.get(result, 'value.blockhash')
|
|
181
120
|
}
|
|
182
121
|
|
|
@@ -188,18 +127,8 @@ export class Api {
|
|
|
188
127
|
])
|
|
189
128
|
}
|
|
190
129
|
|
|
191
|
-
async getPriorityFee(transaction) {
|
|
192
|
-
// https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api
|
|
193
|
-
const result = await this.rpcCall('getPriorityFeeEstimate', [
|
|
194
|
-
{ transaction, options: { recommended: true } },
|
|
195
|
-
])
|
|
196
|
-
return result.priorityFeeEstimate
|
|
197
|
-
}
|
|
198
|
-
|
|
199
130
|
async getBalance(address) {
|
|
200
|
-
const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }]
|
|
201
|
-
address,
|
|
202
|
-
})
|
|
131
|
+
const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }])
|
|
203
132
|
return lodash.get(result, 'value', 0)
|
|
204
133
|
}
|
|
205
134
|
|
|
@@ -240,13 +169,7 @@ export class Api {
|
|
|
240
169
|
const fetchRetry = retry(
|
|
241
170
|
async () => {
|
|
242
171
|
try {
|
|
243
|
-
return await this.rpcCall(
|
|
244
|
-
'getSignaturesForAddress',
|
|
245
|
-
[address, { until, before, limit }],
|
|
246
|
-
{
|
|
247
|
-
address,
|
|
248
|
-
}
|
|
249
|
-
)
|
|
172
|
+
return await this.rpcCall('getSignaturesForAddress', [address, { until, before, limit }])
|
|
250
173
|
} catch (error) {
|
|
251
174
|
if (
|
|
252
175
|
error.message &&
|
|
@@ -353,535 +276,8 @@ export class Api {
|
|
|
353
276
|
return { transactions, newCursor }
|
|
354
277
|
}
|
|
355
278
|
|
|
356
|
-
parseTransaction(
|
|
357
|
-
|
|
358
|
-
txDetails,
|
|
359
|
-
tokenAccountsByOwner,
|
|
360
|
-
{ includeUnparsed = false } = {}
|
|
361
|
-
) {
|
|
362
|
-
let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
|
|
363
|
-
txDetails.meta
|
|
364
|
-
preBalances = preBalances || []
|
|
365
|
-
postBalances = postBalances || []
|
|
366
|
-
preTokenBalances = preTokenBalances || []
|
|
367
|
-
postTokenBalances = postTokenBalances || []
|
|
368
|
-
innerInstructions = innerInstructions || []
|
|
369
|
-
|
|
370
|
-
let { instructions, accountKeys = [] } = txDetails.transaction.message
|
|
371
|
-
const feePayerPubkey = accountKeys[0].pubkey
|
|
372
|
-
const ownerIsFeePayer = feePayerPubkey === ownerAddress
|
|
373
|
-
const txId = txDetails.transaction.signatures[0]
|
|
374
|
-
|
|
375
|
-
const getUnparsedTx = () => {
|
|
376
|
-
const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
|
|
377
|
-
const feePaid = ownerIndex === 0 ? fee : 0
|
|
378
|
-
|
|
379
|
-
return {
|
|
380
|
-
unparsed: true,
|
|
381
|
-
amount:
|
|
382
|
-
ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
|
|
383
|
-
fee: feePaid,
|
|
384
|
-
data: {
|
|
385
|
-
meta: txDetails.meta,
|
|
386
|
-
},
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const getInnerTxsFromBalanceChanges = () => {
|
|
391
|
-
const ownPreTokenBalances = preTokenBalances.filter(
|
|
392
|
-
(balance) => balance.owner === ownerAddress
|
|
393
|
-
)
|
|
394
|
-
const ownPostTokenBalances = postTokenBalances.filter(
|
|
395
|
-
(balance) => balance.owner === ownerAddress
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
return ownPostTokenBalances
|
|
399
|
-
.map((postBalance) => {
|
|
400
|
-
const tokenAccount = tokenAccountsByOwner.find(
|
|
401
|
-
(tokenAccount) => tokenAccount.mintAddress === postBalance.mint
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
const preBalance = ownPreTokenBalances.find(
|
|
405
|
-
(balance) => balance.accountIndex === postBalance.accountIndex
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
const preAmount = BigInt(preBalance?.uiTokenAmount?.amount ?? '0')
|
|
409
|
-
const postAmount = BigInt(postBalance?.uiTokenAmount?.amount ?? '0')
|
|
410
|
-
|
|
411
|
-
const amount = postAmount - preAmount
|
|
412
|
-
|
|
413
|
-
if (!tokenAccount || amount === ZERO) return null
|
|
414
|
-
|
|
415
|
-
// This is not perfect as there could be multiple same-token transfers in single
|
|
416
|
-
// transaction, but our wallet only supports one transaction with single txId
|
|
417
|
-
// so we are picking first that matches (correct token + type - send or receive)
|
|
418
|
-
const match = innerInstructions.find((inner) => {
|
|
419
|
-
const targetOwner = amount < ZERO ? ownerAddress : null
|
|
420
|
-
return (
|
|
421
|
-
inner.token.mintAddress === tokenAccount.mintAddress && targetOwner === inner.owner
|
|
422
|
-
)
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
// It's possible we won't find a match, because our innerInstructions only contain
|
|
426
|
-
// spl-token transfers, but balances of SPL tokens can change in different ways too.
|
|
427
|
-
// for now, we are ignoring this to simplify as those cases are not that common, but
|
|
428
|
-
// they should be handled eventually. It was already a scretch to add unparsed txs logic
|
|
429
|
-
// to existing parser, expanding it further is not going to end well.
|
|
430
|
-
// this probably should be refactored from ground to handle all those transactions
|
|
431
|
-
// as a core part of it in the future
|
|
432
|
-
if (!match) return null
|
|
433
|
-
|
|
434
|
-
const { from, to, owner } = match
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
id: txId,
|
|
438
|
-
slot: txDetails.slot,
|
|
439
|
-
owner,
|
|
440
|
-
from,
|
|
441
|
-
to,
|
|
442
|
-
amount: (amount < ZERO ? -amount : amount).toString(), // inconsistent with the rest, but it can and did overflow
|
|
443
|
-
fee: 0,
|
|
444
|
-
token: tokenAccount,
|
|
445
|
-
data: {
|
|
446
|
-
inner: true,
|
|
447
|
-
},
|
|
448
|
-
}
|
|
449
|
-
})
|
|
450
|
-
.filter((ix) => !!ix)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
instructions = instructions
|
|
454
|
-
.filter((ix) => ix.parsed) // only known instructions
|
|
455
|
-
.map((ix) => ({
|
|
456
|
-
program: ix.program, // system or spl-token
|
|
457
|
-
type: ix.parsed.type, // transfer, createAccount, initializeAccount
|
|
458
|
-
...ix.parsed.info,
|
|
459
|
-
}))
|
|
460
|
-
|
|
461
|
-
let solanaTransferTx = lodash.find(instructions, (ix) => {
|
|
462
|
-
if (![ix.source, ix.destination].includes(ownerAddress)) return false
|
|
463
|
-
return ix.program === 'system' && ix.type === 'transfer'
|
|
464
|
-
}) // get SOL transfer
|
|
465
|
-
|
|
466
|
-
// check if there is a temp account created & closed within the instructions when there is no direct solana transfer
|
|
467
|
-
const accountToRedeemToOwner = solanaTransferTx
|
|
468
|
-
? undefined
|
|
469
|
-
: instructions.find(
|
|
470
|
-
({ type, owner, destination }) =>
|
|
471
|
-
type === 'closeAccount' && owner === ownerAddress && destination === ownerAddress
|
|
472
|
-
)?.account
|
|
473
|
-
|
|
474
|
-
innerInstructions = innerInstructions
|
|
475
|
-
.reduce((acc, val) => {
|
|
476
|
-
return [...acc, ...val.instructions]
|
|
477
|
-
}, [])
|
|
478
|
-
.filter(
|
|
479
|
-
(ix) =>
|
|
480
|
-
ix.parsed &&
|
|
481
|
-
(isSplTransferInstruction({ program: ix.program, type: ix.parsed.type }) ||
|
|
482
|
-
isSplMintInstruction({ program: ix.program, type: ix.parsed.type }) ||
|
|
483
|
-
(!includeUnparsed &&
|
|
484
|
-
isSolTransferInstruction({ program: ix.program, type: ix.parsed.type })))
|
|
485
|
-
)
|
|
486
|
-
.map((ix) => {
|
|
487
|
-
let source = lodash.get(ix, 'parsed.info.source')
|
|
488
|
-
const destination = isSplMintInstruction({ program: ix.program, type: ix.parsed.type })
|
|
489
|
-
? lodash.get(ix, 'parsed.info.account') // only for minting
|
|
490
|
-
: lodash.get(ix, 'parsed.info.destination')
|
|
491
|
-
const amount = Number(
|
|
492
|
-
lodash.get(ix, 'parsed.info.amount', 0) ||
|
|
493
|
-
lodash.get(ix, 'parsed.info.tokenAmount.amount', 0)
|
|
494
|
-
)
|
|
495
|
-
const authority = lodash.get(ix, 'parsed.info.authority')
|
|
496
|
-
|
|
497
|
-
if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
|
|
498
|
-
solanaTransferTx = {
|
|
499
|
-
from: authority || source,
|
|
500
|
-
to: ownerAddress,
|
|
501
|
-
amount,
|
|
502
|
-
fee,
|
|
503
|
-
}
|
|
504
|
-
return
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
source === ownerAddress &&
|
|
509
|
-
isSolTransferInstruction({ program: ix.program, type: ix.parsed.type })
|
|
510
|
-
) {
|
|
511
|
-
const lamports = Number(lodash.get(ix, 'parsed.info.lamports', 0))
|
|
512
|
-
if (solanaTransferTx) {
|
|
513
|
-
solanaTransferTx.lamports += lamports
|
|
514
|
-
solanaTransferTx.amount = solanaTransferTx.lamports
|
|
515
|
-
if (!Array.isArray(solanaTransferTx.to)) {
|
|
516
|
-
solanaTransferTx.data = {
|
|
517
|
-
sent: [
|
|
518
|
-
{
|
|
519
|
-
address: solanaTransferTx.to,
|
|
520
|
-
amount: lamports,
|
|
521
|
-
},
|
|
522
|
-
],
|
|
523
|
-
}
|
|
524
|
-
solanaTransferTx.to = [solanaTransferTx.to]
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
solanaTransferTx.to.push(destination)
|
|
528
|
-
solanaTransferTx.data.sent.push({ address: destination, amount: lamports })
|
|
529
|
-
} else {
|
|
530
|
-
solanaTransferTx = {
|
|
531
|
-
source,
|
|
532
|
-
owner: source,
|
|
533
|
-
from: source,
|
|
534
|
-
to: [destination],
|
|
535
|
-
lamports,
|
|
536
|
-
amount: lamports,
|
|
537
|
-
data: {
|
|
538
|
-
sent: [
|
|
539
|
-
{
|
|
540
|
-
address: destination,
|
|
541
|
-
amount: lamports,
|
|
542
|
-
},
|
|
543
|
-
],
|
|
544
|
-
},
|
|
545
|
-
fee,
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
553
|
-
return [source, destination].includes(tokenAccountAddress)
|
|
554
|
-
})
|
|
555
|
-
if (!tokenAccount) return
|
|
556
|
-
|
|
557
|
-
if (isSplMintInstruction({ program: ix.program, type: ix.parsed.type })) {
|
|
558
|
-
source = lodash.get(ix, 'parsed.info.mintAuthority')
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const isSending = tokenAccountsByOwner.some(({ tokenAccountAddress }) => {
|
|
562
|
-
return [source].includes(tokenAccountAddress)
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
// owner if it's a send tx
|
|
566
|
-
return {
|
|
567
|
-
id: txId,
|
|
568
|
-
program: ix.program,
|
|
569
|
-
type: ix.parsed.type,
|
|
570
|
-
slot: txDetails.slot,
|
|
571
|
-
owner: isSending ? ownerAddress : null,
|
|
572
|
-
from: isSending ? ownerAddress : source,
|
|
573
|
-
to: isSending ? destination : ownerAddress,
|
|
574
|
-
amount,
|
|
575
|
-
token: tokenAccount,
|
|
576
|
-
// Attribute fee only when owner is the actual fee payer
|
|
577
|
-
fee: isSending && ownerIsFeePayer ? fee : 0,
|
|
578
|
-
}
|
|
579
|
-
})
|
|
580
|
-
.filter((ix) => !!ix)
|
|
581
|
-
|
|
582
|
-
// Collect inner instructions into batch sends
|
|
583
|
-
for (let i = 0; i < innerInstructions.length - 1; i++) {
|
|
584
|
-
const tx = innerInstructions[i]
|
|
585
|
-
|
|
586
|
-
for (let j = i + 1; j < innerInstructions.length; j++) {
|
|
587
|
-
const next = innerInstructions[j]
|
|
588
|
-
if (
|
|
589
|
-
tx.id === next.id &&
|
|
590
|
-
tx.token === next.token &&
|
|
591
|
-
tx.owner === ownerAddress &&
|
|
592
|
-
tx.from === next.from
|
|
593
|
-
) {
|
|
594
|
-
if (!tx.data) {
|
|
595
|
-
tx.data = { sent: [{ address: tx.to, amount: tx.amount }] }
|
|
596
|
-
tx.to = [tx.to]
|
|
597
|
-
tx.fee = 0
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
tx.data.sent.push({
|
|
601
|
-
address: next.to,
|
|
602
|
-
amount: next.amount,
|
|
603
|
-
})
|
|
604
|
-
tx.to.push(next.to)
|
|
605
|
-
|
|
606
|
-
tx.amount += next.amount
|
|
607
|
-
|
|
608
|
-
innerInstructions.splice(j, 1)
|
|
609
|
-
j--
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// program:type tells us if it's a SOL or Token transfer
|
|
615
|
-
|
|
616
|
-
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
617
|
-
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
618
|
-
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
619
|
-
|
|
620
|
-
let tx = {}
|
|
621
|
-
if (stakeTx) {
|
|
622
|
-
// start staking
|
|
623
|
-
tx = {
|
|
624
|
-
owner: stakeTx.base,
|
|
625
|
-
from: stakeTx.base,
|
|
626
|
-
to: stakeTx.base,
|
|
627
|
-
amount: stakeTx.lamports,
|
|
628
|
-
fee,
|
|
629
|
-
staking: {
|
|
630
|
-
method: 'createAccountWithSeed',
|
|
631
|
-
seed: stakeTx.seed,
|
|
632
|
-
stakeAddresses: [stakeTx.newAccount],
|
|
633
|
-
stake: stakeTx.lamports,
|
|
634
|
-
},
|
|
635
|
-
}
|
|
636
|
-
} else if (stakeWithdraw) {
|
|
637
|
-
const stakeAccounts = lodash.map(
|
|
638
|
-
lodash.filter(instructions, { program: 'stake', type: 'withdraw' }),
|
|
639
|
-
'stakeAccount'
|
|
640
|
-
)
|
|
641
|
-
tx = {
|
|
642
|
-
owner: stakeWithdraw.withdrawAuthority,
|
|
643
|
-
from: stakeWithdraw.stakeAccount,
|
|
644
|
-
to: stakeWithdraw.destination,
|
|
645
|
-
amount: stakeWithdraw.lamports,
|
|
646
|
-
fee,
|
|
647
|
-
staking: {
|
|
648
|
-
method: 'withdraw',
|
|
649
|
-
stakeAddresses: stakeAccounts,
|
|
650
|
-
stake: stakeWithdraw.lamports,
|
|
651
|
-
},
|
|
652
|
-
}
|
|
653
|
-
} else if (stakeUndelegate) {
|
|
654
|
-
const stakeAccounts = lodash.map(
|
|
655
|
-
lodash.filter(instructions, { program: 'stake', type: 'deactivate' }),
|
|
656
|
-
'stakeAccount'
|
|
657
|
-
)
|
|
658
|
-
tx = {
|
|
659
|
-
owner: stakeUndelegate.stakeAuthority,
|
|
660
|
-
from: stakeUndelegate.stakeAuthority,
|
|
661
|
-
to: stakeUndelegate.stakeAccount, // obsolete
|
|
662
|
-
amount: 0,
|
|
663
|
-
fee,
|
|
664
|
-
staking: {
|
|
665
|
-
method: 'undelegate',
|
|
666
|
-
stakeAddresses: stakeAccounts,
|
|
667
|
-
},
|
|
668
|
-
}
|
|
669
|
-
} else {
|
|
670
|
-
if (solanaTransferTx) {
|
|
671
|
-
const isSending = ownerAddress === solanaTransferTx.source
|
|
672
|
-
tx = {
|
|
673
|
-
owner: solanaTransferTx.source,
|
|
674
|
-
from: solanaTransferTx.source,
|
|
675
|
-
to: solanaTransferTx.destination,
|
|
676
|
-
amount: solanaTransferTx.lamports, // number
|
|
677
|
-
fee: isSending && ownerIsFeePayer ? fee : 0,
|
|
678
|
-
data: solanaTransferTx.data,
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const accountIndexes = accountKeys.reduce((acc, key, i) => {
|
|
683
|
-
const hasKnownOwner = tokenAccountsByOwner.some(
|
|
684
|
-
(tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
acc[i] = {
|
|
688
|
-
...key,
|
|
689
|
-
owner: hasKnownOwner ? ownerAddress : null, // not know (like in an outgoing tx)
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
return acc
|
|
693
|
-
}, Object.create(null)) // { 0: { pubkey, owner }, 1: { ... }, ... }
|
|
694
|
-
|
|
695
|
-
// Parse Token txs
|
|
696
|
-
const tokenTxs = this._parseTokenTransfers({
|
|
697
|
-
instructions,
|
|
698
|
-
innerInstructions,
|
|
699
|
-
tokenAccountsByOwner,
|
|
700
|
-
ownerAddress,
|
|
701
|
-
fee: ownerIsFeePayer ? fee : 0,
|
|
702
|
-
accountIndexes,
|
|
703
|
-
preTokenBalances,
|
|
704
|
-
postTokenBalances,
|
|
705
|
-
})
|
|
706
|
-
|
|
707
|
-
if (tokenTxs.length > 0) {
|
|
708
|
-
// found spl-token simple transfer/transferChecked instruction
|
|
709
|
-
tx.tokenTxs = tokenTxs.map((tx) => ({
|
|
710
|
-
id: txId,
|
|
711
|
-
slot: txDetails.slot,
|
|
712
|
-
...tx,
|
|
713
|
-
}))
|
|
714
|
-
} else if (preTokenBalances && postTokenBalances) {
|
|
715
|
-
// probably a DEX program is involved (multiple instructions), compute balance changes
|
|
716
|
-
// group by owner and supported token
|
|
717
|
-
const preBalances = preTokenBalances.filter((t) => {
|
|
718
|
-
return (
|
|
719
|
-
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
720
|
-
)
|
|
721
|
-
})
|
|
722
|
-
const postBalances = postTokenBalances.filter((t) => {
|
|
723
|
-
return (
|
|
724
|
-
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
725
|
-
)
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
if (preBalances.length > 0 || postBalances.length > 0 || solanaTransferTx) {
|
|
729
|
-
tx = {}
|
|
730
|
-
|
|
731
|
-
if (includeUnparsed && innerInstructions.length > 0) {
|
|
732
|
-
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
733
|
-
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
734
|
-
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
735
|
-
// SOL->SPL swaps on Raydium and Orca.
|
|
736
|
-
tx = getUnparsedTx()
|
|
737
|
-
tx.dexTxs = getInnerTxsFromBalanceChanges()
|
|
738
|
-
} else {
|
|
739
|
-
if (solanaTransferTx) {
|
|
740
|
-
// the base tx will be the one that moved solana.
|
|
741
|
-
tx =
|
|
742
|
-
solanaTransferTx.from && solanaTransferTx.to
|
|
743
|
-
? solanaTransferTx
|
|
744
|
-
: {
|
|
745
|
-
owner: solanaTransferTx.source,
|
|
746
|
-
from: solanaTransferTx.source,
|
|
747
|
-
to: solanaTransferTx.destination,
|
|
748
|
-
amount: solanaTransferTx.lamports, // number
|
|
749
|
-
fee: ownerAddress === solanaTransferTx.source ? fee : 0,
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
754
|
-
if (innerInstructions.length > 0) {
|
|
755
|
-
tx.dexTxs = innerInstructions
|
|
756
|
-
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
757
|
-
if (!tx.from && !solanaTransferTx) {
|
|
758
|
-
tx = tx.dexTxs[0]
|
|
759
|
-
tx.dexTxs = innerInstructions.slice(1)
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const unparsed = Object.keys(tx).length === 0
|
|
768
|
-
|
|
769
|
-
if (unparsed && includeUnparsed) {
|
|
770
|
-
tx = getUnparsedTx()
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// How tokens tx are parsed:
|
|
774
|
-
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
775
|
-
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
776
|
-
// 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
|
|
777
|
-
// QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
|
|
778
|
-
|
|
779
|
-
return {
|
|
780
|
-
id: txId,
|
|
781
|
-
slot: txDetails.slot,
|
|
782
|
-
error: !(txDetails.meta.err === null),
|
|
783
|
-
...tx,
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
_parseTokenTransfers({
|
|
788
|
-
instructions,
|
|
789
|
-
innerInstructions = [],
|
|
790
|
-
tokenAccountsByOwner,
|
|
791
|
-
ownerAddress,
|
|
792
|
-
fee,
|
|
793
|
-
accountIndexes = {},
|
|
794
|
-
preTokenBalances,
|
|
795
|
-
postTokenBalances,
|
|
796
|
-
}) {
|
|
797
|
-
if (
|
|
798
|
-
preTokenBalances.length === 0 &&
|
|
799
|
-
postTokenBalances.length === 0 &&
|
|
800
|
-
!Array.isArray(tokenAccountsByOwner)
|
|
801
|
-
) {
|
|
802
|
-
return []
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const tokenTxs = []
|
|
806
|
-
|
|
807
|
-
instructions.forEach((instruction) => {
|
|
808
|
-
const { type, program, source, destination, amount, tokenAmount } = instruction
|
|
809
|
-
|
|
810
|
-
if (isSplTransferInstruction({ program, type })) {
|
|
811
|
-
let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: source })
|
|
812
|
-
const isSending = !!tokenAccount
|
|
813
|
-
if (!isSending) {
|
|
814
|
-
// receiving
|
|
815
|
-
tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
816
|
-
tokenAccountAddress: destination,
|
|
817
|
-
})
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if (!tokenAccount) return // no transfers with our addresses involved
|
|
821
|
-
|
|
822
|
-
const owner = isSending ? ownerAddress : null
|
|
823
|
-
|
|
824
|
-
delete tokenAccount.balance
|
|
825
|
-
delete tokenAccount.owner
|
|
826
|
-
|
|
827
|
-
// If it's a sending tx we want to have the destination's owner as "to" address
|
|
828
|
-
let to = ownerAddress
|
|
829
|
-
let from = ownerAddress
|
|
830
|
-
if (isSending) {
|
|
831
|
-
to = destination // token account address (trying to get the owner below, we don't always have postTokenBalances...)
|
|
832
|
-
postTokenBalances.forEach((t) => {
|
|
833
|
-
if (accountIndexes[t.accountIndex].pubkey === destination) to = t.owner
|
|
834
|
-
})
|
|
835
|
-
} else {
|
|
836
|
-
// is receiving tx
|
|
837
|
-
from = source // token account address
|
|
838
|
-
preTokenBalances.forEach((t) => {
|
|
839
|
-
if (accountIndexes[t.accountIndex].pubkey === source) from = t.owner
|
|
840
|
-
})
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
tokenTxs.push({
|
|
844
|
-
owner,
|
|
845
|
-
token: tokenAccount,
|
|
846
|
-
from,
|
|
847
|
-
to,
|
|
848
|
-
amount: Number(amount || tokenAmount?.amount || 0), // supporting types: transfer, transferChecked, transferCheckedWithFee
|
|
849
|
-
fee: isSending ? fee : 0, // in lamports
|
|
850
|
-
})
|
|
851
|
-
}
|
|
852
|
-
})
|
|
853
|
-
|
|
854
|
-
innerInstructions.forEach((parsedIx) => {
|
|
855
|
-
const { type, program, amount, from, to } = parsedIx
|
|
856
|
-
|
|
857
|
-
// Handle token minting (mintTo, mintToChecked)
|
|
858
|
-
if (isSplMintInstruction({ program, type })) {
|
|
859
|
-
const {
|
|
860
|
-
token: { tokenAccountAddress },
|
|
861
|
-
} = parsedIx
|
|
862
|
-
|
|
863
|
-
// Check if the destination token account belongs to our owner
|
|
864
|
-
const tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
865
|
-
tokenAccountAddress,
|
|
866
|
-
})
|
|
867
|
-
|
|
868
|
-
if (!tokenAccount) return // not our token account
|
|
869
|
-
|
|
870
|
-
delete tokenAccount.balance
|
|
871
|
-
delete tokenAccount.owner
|
|
872
|
-
|
|
873
|
-
tokenTxs.push({
|
|
874
|
-
owner: null, // no owner for minting (it's created from thin air)
|
|
875
|
-
token: tokenAccount,
|
|
876
|
-
from, // mint address as the source
|
|
877
|
-
to, // our address as recipient
|
|
878
|
-
amount: Number(amount || 0),
|
|
879
|
-
fee: 0, // no fee for receiving minted tokens
|
|
880
|
-
})
|
|
881
|
-
}
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
return tokenTxs
|
|
279
|
+
parseTransaction(...args) {
|
|
280
|
+
return parseTransaction(...args)
|
|
885
281
|
}
|
|
886
282
|
|
|
887
283
|
async getWalletTokensList({ tokenAccounts }) {
|
|
@@ -911,16 +307,16 @@ export class Api {
|
|
|
911
307
|
|
|
912
308
|
async getTokenAccountsByOwner(address, tokenTicker) {
|
|
913
309
|
const [{ value: standardTokenAccounts }, { value: token2022Accounts }] = await Promise.all([
|
|
914
|
-
this.rpcCall(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
{
|
|
918
|
-
),
|
|
919
|
-
this.rpcCall(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
{
|
|
923
|
-
),
|
|
310
|
+
this.rpcCall('getTokenAccountsByOwner', [
|
|
311
|
+
address,
|
|
312
|
+
{ programId: TOKEN_PROGRAM_ID.toBase58() },
|
|
313
|
+
{ encoding: 'jsonParsed' },
|
|
314
|
+
]),
|
|
315
|
+
this.rpcCall('getTokenAccountsByOwner', [
|
|
316
|
+
address,
|
|
317
|
+
{ programId: TOKEN_2022_PROGRAM_ID.toBase58() },
|
|
318
|
+
{ encoding: 'jsonParsed' },
|
|
319
|
+
]),
|
|
924
320
|
])
|
|
925
321
|
|
|
926
322
|
// merge regular token and token2022 program tokens
|
|
@@ -1033,11 +429,10 @@ export class Api {
|
|
|
1033
429
|
}
|
|
1034
430
|
|
|
1035
431
|
async getAccountInfo(address, encoding = 'jsonParsed') {
|
|
1036
|
-
const { value } = await this.rpcCall(
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
)
|
|
432
|
+
const { value } = await this.rpcCall('getAccountInfo', [
|
|
433
|
+
address,
|
|
434
|
+
{ encoding, commitment: 'confirmed' },
|
|
435
|
+
])
|
|
1041
436
|
return value
|
|
1042
437
|
}
|
|
1043
438
|
|
|
@@ -1130,7 +525,7 @@ export class Api {
|
|
|
1130
525
|
encoding: 'jsonParsed',
|
|
1131
526
|
},
|
|
1132
527
|
]
|
|
1133
|
-
const res = await this.rpcCall('getProgramAccounts', params
|
|
528
|
+
const res = await this.rpcCall('getProgramAccounts', params)
|
|
1134
529
|
|
|
1135
530
|
const accounts = {}
|
|
1136
531
|
let totalStake = 0
|
|
@@ -1206,7 +601,7 @@ export class Api {
|
|
|
1206
601
|
const broadcastTxWithRetry = retry(
|
|
1207
602
|
async () => {
|
|
1208
603
|
try {
|
|
1209
|
-
const result = await this.rpcCall('sendTransaction', params
|
|
604
|
+
const result = await this.rpcCall('sendTransaction', params)
|
|
1210
605
|
console.log(`tx ${JSON.stringify(result)} sent!`)
|
|
1211
606
|
|
|
1212
607
|
return result || null
|