@exodus/solana-api 3.25.3 → 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 +26 -0
- package/package.json +3 -3
- package/src/api.js +27 -621
- 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/tx-send.js +2 -2
- 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,532 +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 txId = txDetails.transaction.signatures[0]
|
|
372
|
-
|
|
373
|
-
const getUnparsedTx = () => {
|
|
374
|
-
const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
|
|
375
|
-
const feePaid = ownerIndex === 0 ? fee : 0
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
unparsed: true,
|
|
379
|
-
amount:
|
|
380
|
-
ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
|
|
381
|
-
fee: feePaid,
|
|
382
|
-
data: {
|
|
383
|
-
meta: txDetails.meta,
|
|
384
|
-
},
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const getInnerTxsFromBalanceChanges = () => {
|
|
389
|
-
const ownPreTokenBalances = preTokenBalances.filter(
|
|
390
|
-
(balance) => balance.owner === ownerAddress
|
|
391
|
-
)
|
|
392
|
-
const ownPostTokenBalances = postTokenBalances.filter(
|
|
393
|
-
(balance) => balance.owner === ownerAddress
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
return ownPostTokenBalances
|
|
397
|
-
.map((postBalance) => {
|
|
398
|
-
const tokenAccount = tokenAccountsByOwner.find(
|
|
399
|
-
(tokenAccount) => tokenAccount.mintAddress === postBalance.mint
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
const preBalance = ownPreTokenBalances.find(
|
|
403
|
-
(balance) => balance.accountIndex === postBalance.accountIndex
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
const preAmount = BigInt(preBalance?.uiTokenAmount?.amount ?? '0')
|
|
407
|
-
const postAmount = BigInt(postBalance?.uiTokenAmount?.amount ?? '0')
|
|
408
|
-
|
|
409
|
-
const amount = postAmount - preAmount
|
|
410
|
-
|
|
411
|
-
if (!tokenAccount || amount === ZERO) return null
|
|
412
|
-
|
|
413
|
-
// This is not perfect as there could be multiple same-token transfers in single
|
|
414
|
-
// transaction, but our wallet only supports one transaction with single txId
|
|
415
|
-
// so we are picking first that matches (correct token + type - send or receive)
|
|
416
|
-
const match = innerInstructions.find((inner) => {
|
|
417
|
-
const targetOwner = amount < ZERO ? ownerAddress : null
|
|
418
|
-
return (
|
|
419
|
-
inner.token.mintAddress === tokenAccount.mintAddress && targetOwner === inner.owner
|
|
420
|
-
)
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
// It's possible we won't find a match, because our innerInstructions only contain
|
|
424
|
-
// spl-token transfers, but balances of SPL tokens can change in different ways too.
|
|
425
|
-
// for now, we are ignoring this to simplify as those cases are not that common, but
|
|
426
|
-
// they should be handled eventually. It was already a scretch to add unparsed txs logic
|
|
427
|
-
// to existing parser, expanding it further is not going to end well.
|
|
428
|
-
// this probably should be refactored from ground to handle all those transactions
|
|
429
|
-
// as a core part of it in the future
|
|
430
|
-
if (!match) return null
|
|
431
|
-
|
|
432
|
-
const { from, to, owner } = match
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
id: txId,
|
|
436
|
-
slot: txDetails.slot,
|
|
437
|
-
owner,
|
|
438
|
-
from,
|
|
439
|
-
to,
|
|
440
|
-
amount: (amount < ZERO ? -amount : amount).toString(), // inconsistent with the rest, but it can and did overflow
|
|
441
|
-
fee: 0,
|
|
442
|
-
token: tokenAccount,
|
|
443
|
-
data: {
|
|
444
|
-
inner: true,
|
|
445
|
-
},
|
|
446
|
-
}
|
|
447
|
-
})
|
|
448
|
-
.filter((ix) => !!ix)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
instructions = instructions
|
|
452
|
-
.filter((ix) => ix.parsed) // only known instructions
|
|
453
|
-
.map((ix) => ({
|
|
454
|
-
program: ix.program, // system or spl-token
|
|
455
|
-
type: ix.parsed.type, // transfer, createAccount, initializeAccount
|
|
456
|
-
...ix.parsed.info,
|
|
457
|
-
}))
|
|
458
|
-
|
|
459
|
-
let solanaTransferTx = lodash.find(instructions, (ix) => {
|
|
460
|
-
if (![ix.source, ix.destination].includes(ownerAddress)) return false
|
|
461
|
-
return ix.program === 'system' && ix.type === 'transfer'
|
|
462
|
-
}) // get SOL transfer
|
|
463
|
-
|
|
464
|
-
// check if there is a temp account created & closed within the instructions when there is no direct solana transfer
|
|
465
|
-
const accountToRedeemToOwner = solanaTransferTx
|
|
466
|
-
? undefined
|
|
467
|
-
: instructions.find(
|
|
468
|
-
({ type, owner, destination }) =>
|
|
469
|
-
type === 'closeAccount' && owner === ownerAddress && destination === ownerAddress
|
|
470
|
-
)?.account
|
|
471
|
-
|
|
472
|
-
innerInstructions = innerInstructions
|
|
473
|
-
.reduce((acc, val) => {
|
|
474
|
-
return [...acc, ...val.instructions]
|
|
475
|
-
}, [])
|
|
476
|
-
.filter(
|
|
477
|
-
(ix) =>
|
|
478
|
-
ix.parsed &&
|
|
479
|
-
(isSplTransferInstruction({ program: ix.program, type: ix.parsed.type }) ||
|
|
480
|
-
isSplMintInstruction({ program: ix.program, type: ix.parsed.type }) ||
|
|
481
|
-
(!includeUnparsed &&
|
|
482
|
-
isSolTransferInstruction({ program: ix.program, type: ix.parsed.type })))
|
|
483
|
-
)
|
|
484
|
-
.map((ix) => {
|
|
485
|
-
let source = lodash.get(ix, 'parsed.info.source')
|
|
486
|
-
const destination = isSplMintInstruction({ program: ix.program, type: ix.parsed.type })
|
|
487
|
-
? lodash.get(ix, 'parsed.info.account') // only for minting
|
|
488
|
-
: lodash.get(ix, 'parsed.info.destination')
|
|
489
|
-
const amount = Number(
|
|
490
|
-
lodash.get(ix, 'parsed.info.amount', 0) ||
|
|
491
|
-
lodash.get(ix, 'parsed.info.tokenAmount.amount', 0)
|
|
492
|
-
)
|
|
493
|
-
const authority = lodash.get(ix, 'parsed.info.authority')
|
|
494
|
-
|
|
495
|
-
if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
|
|
496
|
-
solanaTransferTx = {
|
|
497
|
-
from: authority || source,
|
|
498
|
-
to: ownerAddress,
|
|
499
|
-
amount,
|
|
500
|
-
fee,
|
|
501
|
-
}
|
|
502
|
-
return
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (
|
|
506
|
-
source === ownerAddress &&
|
|
507
|
-
isSolTransferInstruction({ program: ix.program, type: ix.parsed.type })
|
|
508
|
-
) {
|
|
509
|
-
const lamports = Number(lodash.get(ix, 'parsed.info.lamports', 0))
|
|
510
|
-
if (solanaTransferTx) {
|
|
511
|
-
solanaTransferTx.lamports += lamports
|
|
512
|
-
solanaTransferTx.amount = solanaTransferTx.lamports
|
|
513
|
-
if (!Array.isArray(solanaTransferTx.to)) {
|
|
514
|
-
solanaTransferTx.data = {
|
|
515
|
-
sent: [
|
|
516
|
-
{
|
|
517
|
-
address: solanaTransferTx.to,
|
|
518
|
-
amount: lamports,
|
|
519
|
-
},
|
|
520
|
-
],
|
|
521
|
-
}
|
|
522
|
-
solanaTransferTx.to = [solanaTransferTx.to]
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
solanaTransferTx.to.push(destination)
|
|
526
|
-
solanaTransferTx.data.sent.push({ address: destination, amount: lamports })
|
|
527
|
-
} else {
|
|
528
|
-
solanaTransferTx = {
|
|
529
|
-
source,
|
|
530
|
-
owner: source,
|
|
531
|
-
from: source,
|
|
532
|
-
to: [destination],
|
|
533
|
-
lamports,
|
|
534
|
-
amount: lamports,
|
|
535
|
-
data: {
|
|
536
|
-
sent: [
|
|
537
|
-
{
|
|
538
|
-
address: destination,
|
|
539
|
-
amount: lamports,
|
|
540
|
-
},
|
|
541
|
-
],
|
|
542
|
-
},
|
|
543
|
-
fee,
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
551
|
-
return [source, destination].includes(tokenAccountAddress)
|
|
552
|
-
})
|
|
553
|
-
if (!tokenAccount) return
|
|
554
|
-
|
|
555
|
-
if (isSplMintInstruction({ program: ix.program, type: ix.parsed.type })) {
|
|
556
|
-
source = lodash.get(ix, 'parsed.info.mintAuthority')
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const isSending = tokenAccountsByOwner.some(({ tokenAccountAddress }) => {
|
|
560
|
-
return [source].includes(tokenAccountAddress)
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
// owner if it's a send tx
|
|
564
|
-
return {
|
|
565
|
-
id: txId,
|
|
566
|
-
program: ix.program,
|
|
567
|
-
type: ix.parsed.type,
|
|
568
|
-
slot: txDetails.slot,
|
|
569
|
-
owner: isSending ? ownerAddress : null,
|
|
570
|
-
from: isSending ? ownerAddress : source,
|
|
571
|
-
to: isSending ? destination : ownerAddress,
|
|
572
|
-
amount,
|
|
573
|
-
token: tokenAccount,
|
|
574
|
-
fee: isSending ? fee : 0,
|
|
575
|
-
}
|
|
576
|
-
})
|
|
577
|
-
.filter((ix) => !!ix)
|
|
578
|
-
|
|
579
|
-
// Collect inner instructions into batch sends
|
|
580
|
-
for (let i = 0; i < innerInstructions.length - 1; i++) {
|
|
581
|
-
const tx = innerInstructions[i]
|
|
582
|
-
|
|
583
|
-
for (let j = i + 1; j < innerInstructions.length; j++) {
|
|
584
|
-
const next = innerInstructions[j]
|
|
585
|
-
if (
|
|
586
|
-
tx.id === next.id &&
|
|
587
|
-
tx.token === next.token &&
|
|
588
|
-
tx.owner === ownerAddress &&
|
|
589
|
-
tx.from === next.from
|
|
590
|
-
) {
|
|
591
|
-
if (!tx.data) {
|
|
592
|
-
tx.data = { sent: [{ address: tx.to, amount: tx.amount }] }
|
|
593
|
-
tx.to = [tx.to]
|
|
594
|
-
tx.fee = 0
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
tx.data.sent.push({
|
|
598
|
-
address: next.to,
|
|
599
|
-
amount: next.amount,
|
|
600
|
-
})
|
|
601
|
-
tx.to.push(next.to)
|
|
602
|
-
|
|
603
|
-
tx.amount += next.amount
|
|
604
|
-
|
|
605
|
-
innerInstructions.splice(j, 1)
|
|
606
|
-
j--
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// program:type tells us if it's a SOL or Token transfer
|
|
612
|
-
|
|
613
|
-
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
614
|
-
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
615
|
-
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
616
|
-
|
|
617
|
-
let tx = {}
|
|
618
|
-
if (stakeTx) {
|
|
619
|
-
// start staking
|
|
620
|
-
tx = {
|
|
621
|
-
owner: stakeTx.base,
|
|
622
|
-
from: stakeTx.base,
|
|
623
|
-
to: stakeTx.base,
|
|
624
|
-
amount: stakeTx.lamports,
|
|
625
|
-
fee,
|
|
626
|
-
staking: {
|
|
627
|
-
method: 'createAccountWithSeed',
|
|
628
|
-
seed: stakeTx.seed,
|
|
629
|
-
stakeAddresses: [stakeTx.newAccount],
|
|
630
|
-
stake: stakeTx.lamports,
|
|
631
|
-
},
|
|
632
|
-
}
|
|
633
|
-
} else if (stakeWithdraw) {
|
|
634
|
-
const stakeAccounts = lodash.map(
|
|
635
|
-
lodash.filter(instructions, { program: 'stake', type: 'withdraw' }),
|
|
636
|
-
'stakeAccount'
|
|
637
|
-
)
|
|
638
|
-
tx = {
|
|
639
|
-
owner: stakeWithdraw.withdrawAuthority,
|
|
640
|
-
from: stakeWithdraw.stakeAccount,
|
|
641
|
-
to: stakeWithdraw.destination,
|
|
642
|
-
amount: stakeWithdraw.lamports,
|
|
643
|
-
fee,
|
|
644
|
-
staking: {
|
|
645
|
-
method: 'withdraw',
|
|
646
|
-
stakeAddresses: stakeAccounts,
|
|
647
|
-
stake: stakeWithdraw.lamports,
|
|
648
|
-
},
|
|
649
|
-
}
|
|
650
|
-
} else if (stakeUndelegate) {
|
|
651
|
-
const stakeAccounts = lodash.map(
|
|
652
|
-
lodash.filter(instructions, { program: 'stake', type: 'deactivate' }),
|
|
653
|
-
'stakeAccount'
|
|
654
|
-
)
|
|
655
|
-
tx = {
|
|
656
|
-
owner: stakeUndelegate.stakeAuthority,
|
|
657
|
-
from: stakeUndelegate.stakeAuthority,
|
|
658
|
-
to: stakeUndelegate.stakeAccount, // obsolete
|
|
659
|
-
amount: 0,
|
|
660
|
-
fee,
|
|
661
|
-
staking: {
|
|
662
|
-
method: 'undelegate',
|
|
663
|
-
stakeAddresses: stakeAccounts,
|
|
664
|
-
},
|
|
665
|
-
}
|
|
666
|
-
} else {
|
|
667
|
-
if (solanaTransferTx) {
|
|
668
|
-
const isSending = ownerAddress === solanaTransferTx.source
|
|
669
|
-
tx = {
|
|
670
|
-
owner: solanaTransferTx.source,
|
|
671
|
-
from: solanaTransferTx.source,
|
|
672
|
-
to: solanaTransferTx.destination,
|
|
673
|
-
amount: solanaTransferTx.lamports, // number
|
|
674
|
-
fee: isSending ? fee : 0,
|
|
675
|
-
data: solanaTransferTx.data,
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const accountIndexes = accountKeys.reduce((acc, key, i) => {
|
|
680
|
-
const hasKnownOwner = tokenAccountsByOwner.some(
|
|
681
|
-
(tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
acc[i] = {
|
|
685
|
-
...key,
|
|
686
|
-
owner: hasKnownOwner ? ownerAddress : null, // not know (like in an outgoing tx)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return acc
|
|
690
|
-
}, Object.create(null)) // { 0: { pubkey, owner }, 1: { ... }, ... }
|
|
691
|
-
|
|
692
|
-
// Parse Token txs
|
|
693
|
-
const tokenTxs = this._parseTokenTransfers({
|
|
694
|
-
instructions,
|
|
695
|
-
innerInstructions,
|
|
696
|
-
tokenAccountsByOwner,
|
|
697
|
-
ownerAddress,
|
|
698
|
-
fee,
|
|
699
|
-
accountIndexes,
|
|
700
|
-
preTokenBalances,
|
|
701
|
-
postTokenBalances,
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
if (tokenTxs.length > 0) {
|
|
705
|
-
// found spl-token simple transfer/transferChecked instruction
|
|
706
|
-
tx.tokenTxs = tokenTxs.map((tx) => ({
|
|
707
|
-
id: txId,
|
|
708
|
-
slot: txDetails.slot,
|
|
709
|
-
...tx,
|
|
710
|
-
}))
|
|
711
|
-
} else if (preTokenBalances && postTokenBalances) {
|
|
712
|
-
// probably a DEX program is involved (multiple instructions), compute balance changes
|
|
713
|
-
// group by owner and supported token
|
|
714
|
-
const preBalances = preTokenBalances.filter((t) => {
|
|
715
|
-
return (
|
|
716
|
-
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
717
|
-
)
|
|
718
|
-
})
|
|
719
|
-
const postBalances = postTokenBalances.filter((t) => {
|
|
720
|
-
return (
|
|
721
|
-
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
722
|
-
)
|
|
723
|
-
})
|
|
724
|
-
|
|
725
|
-
if (preBalances.length > 0 || postBalances.length > 0 || solanaTransferTx) {
|
|
726
|
-
tx = {}
|
|
727
|
-
|
|
728
|
-
if (includeUnparsed && innerInstructions.length > 0) {
|
|
729
|
-
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
730
|
-
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
731
|
-
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
732
|
-
// SOL->SPL swaps on Raydium and Orca.
|
|
733
|
-
tx = getUnparsedTx()
|
|
734
|
-
tx.dexTxs = getInnerTxsFromBalanceChanges()
|
|
735
|
-
} else {
|
|
736
|
-
if (solanaTransferTx) {
|
|
737
|
-
// the base tx will be the one that moved solana.
|
|
738
|
-
tx =
|
|
739
|
-
solanaTransferTx.from && solanaTransferTx.to
|
|
740
|
-
? solanaTransferTx
|
|
741
|
-
: {
|
|
742
|
-
owner: solanaTransferTx.source,
|
|
743
|
-
from: solanaTransferTx.source,
|
|
744
|
-
to: solanaTransferTx.destination,
|
|
745
|
-
amount: solanaTransferTx.lamports, // number
|
|
746
|
-
fee: ownerAddress === solanaTransferTx.source ? fee : 0,
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
751
|
-
if (innerInstructions.length > 0) {
|
|
752
|
-
tx.dexTxs = innerInstructions
|
|
753
|
-
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
754
|
-
if (!tx.from && !solanaTransferTx) {
|
|
755
|
-
tx = tx.dexTxs[0]
|
|
756
|
-
tx.dexTxs = innerInstructions.slice(1)
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
const unparsed = Object.keys(tx).length === 0
|
|
765
|
-
|
|
766
|
-
if (unparsed && includeUnparsed) {
|
|
767
|
-
tx = getUnparsedTx()
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// How tokens tx are parsed:
|
|
771
|
-
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
772
|
-
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
773
|
-
// 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
|
|
774
|
-
// QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
|
|
775
|
-
|
|
776
|
-
return {
|
|
777
|
-
id: txDetails.transaction.signatures[0],
|
|
778
|
-
slot: txDetails.slot,
|
|
779
|
-
error: !(txDetails.meta.err === null),
|
|
780
|
-
...tx,
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
_parseTokenTransfers({
|
|
785
|
-
instructions,
|
|
786
|
-
innerInstructions = [],
|
|
787
|
-
tokenAccountsByOwner,
|
|
788
|
-
ownerAddress,
|
|
789
|
-
fee,
|
|
790
|
-
accountIndexes = {},
|
|
791
|
-
preTokenBalances,
|
|
792
|
-
postTokenBalances,
|
|
793
|
-
}) {
|
|
794
|
-
if (
|
|
795
|
-
preTokenBalances.length === 0 &&
|
|
796
|
-
postTokenBalances.length === 0 &&
|
|
797
|
-
!Array.isArray(tokenAccountsByOwner)
|
|
798
|
-
) {
|
|
799
|
-
return []
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const tokenTxs = []
|
|
803
|
-
|
|
804
|
-
instructions.forEach((instruction) => {
|
|
805
|
-
const { type, program, source, destination, amount, tokenAmount } = instruction
|
|
806
|
-
|
|
807
|
-
if (isSplTransferInstruction({ program, type })) {
|
|
808
|
-
let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: source })
|
|
809
|
-
const isSending = !!tokenAccount
|
|
810
|
-
if (!isSending) {
|
|
811
|
-
// receiving
|
|
812
|
-
tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
813
|
-
tokenAccountAddress: destination,
|
|
814
|
-
})
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
if (!tokenAccount) return // no transfers with our addresses involved
|
|
818
|
-
|
|
819
|
-
const owner = isSending ? ownerAddress : null
|
|
820
|
-
|
|
821
|
-
delete tokenAccount.balance
|
|
822
|
-
delete tokenAccount.owner
|
|
823
|
-
|
|
824
|
-
// If it's a sending tx we want to have the destination's owner as "to" address
|
|
825
|
-
let to = ownerAddress
|
|
826
|
-
let from = ownerAddress
|
|
827
|
-
if (isSending) {
|
|
828
|
-
to = destination // token account address (trying to get the owner below, we don't always have postTokenBalances...)
|
|
829
|
-
postTokenBalances.forEach((t) => {
|
|
830
|
-
if (accountIndexes[t.accountIndex].pubkey === destination) to = t.owner
|
|
831
|
-
})
|
|
832
|
-
} else {
|
|
833
|
-
// is receiving tx
|
|
834
|
-
from = source // token account address
|
|
835
|
-
preTokenBalances.forEach((t) => {
|
|
836
|
-
if (accountIndexes[t.accountIndex].pubkey === source) from = t.owner
|
|
837
|
-
})
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
tokenTxs.push({
|
|
841
|
-
owner,
|
|
842
|
-
token: tokenAccount,
|
|
843
|
-
from,
|
|
844
|
-
to,
|
|
845
|
-
amount: Number(amount || tokenAmount?.amount || 0), // supporting types: transfer, transferChecked, transferCheckedWithFee
|
|
846
|
-
fee: isSending ? fee : 0, // in lamports
|
|
847
|
-
})
|
|
848
|
-
}
|
|
849
|
-
})
|
|
850
|
-
|
|
851
|
-
innerInstructions.forEach((parsedIx) => {
|
|
852
|
-
const { type, program, amount, from, to } = parsedIx
|
|
853
|
-
|
|
854
|
-
// Handle token minting (mintTo, mintToChecked)
|
|
855
|
-
if (isSplMintInstruction({ program, type })) {
|
|
856
|
-
const {
|
|
857
|
-
token: { tokenAccountAddress },
|
|
858
|
-
} = parsedIx
|
|
859
|
-
|
|
860
|
-
// Check if the destination token account belongs to our owner
|
|
861
|
-
const tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
862
|
-
tokenAccountAddress,
|
|
863
|
-
})
|
|
864
|
-
|
|
865
|
-
if (!tokenAccount) return // not our token account
|
|
866
|
-
|
|
867
|
-
delete tokenAccount.balance
|
|
868
|
-
delete tokenAccount.owner
|
|
869
|
-
|
|
870
|
-
tokenTxs.push({
|
|
871
|
-
owner: null, // no owner for minting (it's created from thin air)
|
|
872
|
-
token: tokenAccount,
|
|
873
|
-
from, // mint address as the source
|
|
874
|
-
to, // our address as recipient
|
|
875
|
-
amount: Number(amount || 0),
|
|
876
|
-
fee: 0, // no fee for receiving minted tokens
|
|
877
|
-
})
|
|
878
|
-
}
|
|
879
|
-
})
|
|
880
|
-
|
|
881
|
-
return tokenTxs
|
|
287
|
+
parseTransaction(...args) {
|
|
288
|
+
return parseTransaction(...args)
|
|
882
289
|
}
|
|
883
290
|
|
|
884
291
|
async getWalletTokensList({ tokenAccounts }) {
|
|
@@ -908,16 +315,16 @@ export class Api {
|
|
|
908
315
|
|
|
909
316
|
async getTokenAccountsByOwner(address, tokenTicker) {
|
|
910
317
|
const [{ value: standardTokenAccounts }, { value: token2022Accounts }] = await Promise.all([
|
|
911
|
-
this.rpcCall(
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
{
|
|
915
|
-
),
|
|
916
|
-
this.rpcCall(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
{
|
|
920
|
-
),
|
|
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
|
+
]),
|
|
921
328
|
])
|
|
922
329
|
|
|
923
330
|
// merge regular token and token2022 program tokens
|
|
@@ -1030,11 +437,10 @@ export class Api {
|
|
|
1030
437
|
}
|
|
1031
438
|
|
|
1032
439
|
async getAccountInfo(address, encoding = 'jsonParsed') {
|
|
1033
|
-
const { value } = await this.rpcCall(
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
)
|
|
440
|
+
const { value } = await this.rpcCall('getAccountInfo', [
|
|
441
|
+
address,
|
|
442
|
+
{ encoding, commitment: 'confirmed' },
|
|
443
|
+
])
|
|
1038
444
|
return value
|
|
1039
445
|
}
|
|
1040
446
|
|
|
@@ -1127,7 +533,7 @@ export class Api {
|
|
|
1127
533
|
encoding: 'jsonParsed',
|
|
1128
534
|
},
|
|
1129
535
|
]
|
|
1130
|
-
const res = await this.rpcCall('getProgramAccounts', params
|
|
536
|
+
const res = await this.rpcCall('getProgramAccounts', params)
|
|
1131
537
|
|
|
1132
538
|
const accounts = {}
|
|
1133
539
|
let totalStake = 0
|
|
@@ -1203,7 +609,7 @@ export class Api {
|
|
|
1203
609
|
const broadcastTxWithRetry = retry(
|
|
1204
610
|
async () => {
|
|
1205
611
|
try {
|
|
1206
|
-
const result = await this.rpcCall('sendTransaction', params
|
|
612
|
+
const result = await this.rpcCall('sendTransaction', params)
|
|
1207
613
|
console.log(`tx ${JSON.stringify(result)} sent!`)
|
|
1208
614
|
|
|
1209
615
|
return result || null
|