@exodus/solana-api 3.25.4 → 3.26.0
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 +16 -0
- package/package.json +3 -3
- package/src/api.js +27 -624
- package/src/connection.js +42 -111
- package/src/index.js +2 -0
- package/src/tx-log/README.md +63 -0
- package/src/tx-log/clarity-monitor.js +6 -8
- package/src/tx-log/index.js +3 -2
- package/src/tx-log/solana-monitor.js +10 -10
- 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
|
|
|
@@ -197,9 +136,7 @@ export class Api {
|
|
|
197
136
|
}
|
|
198
137
|
|
|
199
138
|
async getBalance(address) {
|
|
200
|
-
const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }]
|
|
201
|
-
address,
|
|
202
|
-
})
|
|
139
|
+
const result = await this.rpcCall('getBalance', [address, { encoding: 'jsonParsed' }])
|
|
203
140
|
return lodash.get(result, 'value', 0)
|
|
204
141
|
}
|
|
205
142
|
|
|
@@ -240,13 +177,7 @@ export class Api {
|
|
|
240
177
|
const fetchRetry = retry(
|
|
241
178
|
async () => {
|
|
242
179
|
try {
|
|
243
|
-
return await this.rpcCall(
|
|
244
|
-
'getSignaturesForAddress',
|
|
245
|
-
[address, { until, before, limit }],
|
|
246
|
-
{
|
|
247
|
-
address,
|
|
248
|
-
}
|
|
249
|
-
)
|
|
180
|
+
return await this.rpcCall('getSignaturesForAddress', [address, { until, before, limit }])
|
|
250
181
|
} catch (error) {
|
|
251
182
|
if (
|
|
252
183
|
error.message &&
|
|
@@ -353,535 +284,8 @@ export class Api {
|
|
|
353
284
|
return { transactions, newCursor }
|
|
354
285
|
}
|
|
355
286
|
|
|
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
|
|
287
|
+
parseTransaction(...args) {
|
|
288
|
+
return parseTransaction(...args)
|
|
885
289
|
}
|
|
886
290
|
|
|
887
291
|
async getWalletTokensList({ tokenAccounts }) {
|
|
@@ -911,16 +315,16 @@ export class Api {
|
|
|
911
315
|
|
|
912
316
|
async getTokenAccountsByOwner(address, tokenTicker) {
|
|
913
317
|
const [{ value: standardTokenAccounts }, { value: token2022Accounts }] = await Promise.all([
|
|
914
|
-
this.rpcCall(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
{
|
|
918
|
-
),
|
|
919
|
-
this.rpcCall(
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
{
|
|
923
|
-
),
|
|
318
|
+
this.rpcCall('getTokenAccountsByOwner', [
|
|
319
|
+
address,
|
|
320
|
+
{ programId: TOKEN_PROGRAM_ID.toBase58() },
|
|
321
|
+
{ encoding: 'jsonParsed' },
|
|
322
|
+
]),
|
|
323
|
+
this.rpcCall('getTokenAccountsByOwner', [
|
|
324
|
+
address,
|
|
325
|
+
{ programId: TOKEN_2022_PROGRAM_ID.toBase58() },
|
|
326
|
+
{ encoding: 'jsonParsed' },
|
|
327
|
+
]),
|
|
924
328
|
])
|
|
925
329
|
|
|
926
330
|
// merge regular token and token2022 program tokens
|
|
@@ -1033,11 +437,10 @@ export class Api {
|
|
|
1033
437
|
}
|
|
1034
438
|
|
|
1035
439
|
async getAccountInfo(address, encoding = 'jsonParsed') {
|
|
1036
|
-
const { value } = await this.rpcCall(
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
)
|
|
440
|
+
const { value } = await this.rpcCall('getAccountInfo', [
|
|
441
|
+
address,
|
|
442
|
+
{ encoding, commitment: 'confirmed' },
|
|
443
|
+
])
|
|
1041
444
|
return value
|
|
1042
445
|
}
|
|
1043
446
|
|
|
@@ -1130,7 +533,7 @@ export class Api {
|
|
|
1130
533
|
encoding: 'jsonParsed',
|
|
1131
534
|
},
|
|
1132
535
|
]
|
|
1133
|
-
const res = await this.rpcCall('getProgramAccounts', params
|
|
536
|
+
const res = await this.rpcCall('getProgramAccounts', params)
|
|
1134
537
|
|
|
1135
538
|
const accounts = {}
|
|
1136
539
|
let totalStake = 0
|
|
@@ -1206,7 +609,7 @@ export class Api {
|
|
|
1206
609
|
const broadcastTxWithRetry = retry(
|
|
1207
610
|
async () => {
|
|
1208
611
|
try {
|
|
1209
|
-
const result = await this.rpcCall('sendTransaction', params
|
|
612
|
+
const result = await this.rpcCall('sendTransaction', params)
|
|
1210
613
|
console.log(`tx ${JSON.stringify(result)} sent!`)
|
|
1211
614
|
|
|
1212
615
|
return result || null
|