@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/tx-parser.js
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import lodash from 'lodash'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isSolTransferInstruction,
|
|
5
|
+
isSplMintInstruction,
|
|
6
|
+
isSplTransferInstruction,
|
|
7
|
+
} from './txs-utils.js'
|
|
8
|
+
|
|
9
|
+
const ZERO = BigInt(0)
|
|
10
|
+
|
|
11
|
+
export const parseTransaction = (
|
|
12
|
+
ownerAddress,
|
|
13
|
+
txDetails,
|
|
14
|
+
tokenAccountsByOwner,
|
|
15
|
+
{ includeUnparsed = false } = {}
|
|
16
|
+
) => {
|
|
17
|
+
let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
|
|
18
|
+
txDetails.meta
|
|
19
|
+
preBalances = preBalances || []
|
|
20
|
+
postBalances = postBalances || []
|
|
21
|
+
preTokenBalances = preTokenBalances || []
|
|
22
|
+
postTokenBalances = postTokenBalances || []
|
|
23
|
+
innerInstructions = innerInstructions || []
|
|
24
|
+
|
|
25
|
+
let { instructions, accountKeys = [] } = txDetails.transaction.message
|
|
26
|
+
const feePayerPubkey = accountKeys[0].pubkey
|
|
27
|
+
const ownerIsFeePayer = feePayerPubkey === ownerAddress
|
|
28
|
+
const txId = txDetails.transaction.signatures[0]
|
|
29
|
+
|
|
30
|
+
const getUnparsedTx = () => {
|
|
31
|
+
const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
|
|
32
|
+
const feePaid = ownerIndex === 0 ? fee : 0
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
unparsed: true,
|
|
36
|
+
amount: ownerIndex === -1 ? 0 : postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
|
|
37
|
+
fee: feePaid,
|
|
38
|
+
data: {
|
|
39
|
+
meta: txDetails.meta,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const getInnerTxsFromBalanceChanges = () => {
|
|
45
|
+
const ownPreTokenBalances = preTokenBalances.filter((balance) => balance.owner === ownerAddress)
|
|
46
|
+
const ownPostTokenBalances = postTokenBalances.filter(
|
|
47
|
+
(balance) => balance.owner === ownerAddress
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return ownPostTokenBalances
|
|
51
|
+
.map((postBalance) => {
|
|
52
|
+
const tokenAccount = tokenAccountsByOwner.find(
|
|
53
|
+
(tokenAccount) => tokenAccount.mintAddress === postBalance.mint
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const preBalance = ownPreTokenBalances.find(
|
|
57
|
+
(balance) => balance.accountIndex === postBalance.accountIndex
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const preAmount = BigInt(preBalance?.uiTokenAmount?.amount ?? '0')
|
|
61
|
+
const postAmount = BigInt(postBalance?.uiTokenAmount?.amount ?? '0')
|
|
62
|
+
|
|
63
|
+
const amount = postAmount - preAmount
|
|
64
|
+
|
|
65
|
+
if (!tokenAccount || amount === ZERO) return null
|
|
66
|
+
|
|
67
|
+
// This is not perfect as there could be multiple same-token transfers in single
|
|
68
|
+
// transaction, but our wallet only supports one transaction with single txId
|
|
69
|
+
// so we are picking first that matches (correct token + type - send or receive)
|
|
70
|
+
const match = innerInstructions.find((inner) => {
|
|
71
|
+
const targetOwner = amount < ZERO ? ownerAddress : null
|
|
72
|
+
return inner.token.mintAddress === tokenAccount.mintAddress && targetOwner === inner.owner
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// It's possible we won't find a match, because our innerInstructions only contain
|
|
76
|
+
// spl-token transfers, but balances of SPL tokens can change in different ways too.
|
|
77
|
+
// for now, we are ignoring this to simplify as those cases are not that common, but
|
|
78
|
+
// they should be handled eventually. It was already a scretch to add unparsed txs logic
|
|
79
|
+
// to existing parser, expanding it further is not going to end well.
|
|
80
|
+
// this probably should be refactored from ground to handle all those transactions
|
|
81
|
+
// as a core part of it in the future
|
|
82
|
+
if (!match) return null
|
|
83
|
+
|
|
84
|
+
const { from, to, owner } = match
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
id: txId,
|
|
88
|
+
slot: txDetails.slot,
|
|
89
|
+
error: !(txDetails.meta.err === null),
|
|
90
|
+
owner,
|
|
91
|
+
from,
|
|
92
|
+
to,
|
|
93
|
+
amount: (amount < ZERO ? -amount : amount).toString(), // inconsistent with the rest, but it can and did overflow
|
|
94
|
+
fee: 0,
|
|
95
|
+
token: tokenAccount,
|
|
96
|
+
data: {
|
|
97
|
+
inner: true,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.filter((ix) => !!ix)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
instructions = instructions
|
|
105
|
+
.filter((ix) => ix.parsed) // only known instructions
|
|
106
|
+
.map((ix) => ({
|
|
107
|
+
program: ix.program, // system or spl-token
|
|
108
|
+
type: ix.parsed.type, // transfer, createAccount, initializeAccount
|
|
109
|
+
...ix.parsed.info,
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
let solanaTransferTx = lodash.find(instructions, (ix) => {
|
|
113
|
+
if (![ix.source, ix.destination].includes(ownerAddress)) return false
|
|
114
|
+
return ix.program === 'system' && ix.type === 'transfer'
|
|
115
|
+
}) // get SOL transfer
|
|
116
|
+
|
|
117
|
+
// check if there is a temp account created & closed within the instructions when there is no direct solana transfer
|
|
118
|
+
const accountToRedeemToOwner = solanaTransferTx
|
|
119
|
+
? undefined
|
|
120
|
+
: instructions.find(
|
|
121
|
+
({ type, owner, destination }) =>
|
|
122
|
+
type === 'closeAccount' && owner === ownerAddress && destination === ownerAddress
|
|
123
|
+
)?.account
|
|
124
|
+
|
|
125
|
+
innerInstructions = innerInstructions
|
|
126
|
+
.reduce((acc, val) => {
|
|
127
|
+
return [...acc, ...val.instructions]
|
|
128
|
+
}, [])
|
|
129
|
+
.filter(
|
|
130
|
+
(ix) =>
|
|
131
|
+
ix.parsed &&
|
|
132
|
+
(isSplTransferInstruction({ program: ix.program, type: ix.parsed.type }) ||
|
|
133
|
+
isSplMintInstruction({ program: ix.program, type: ix.parsed.type }) ||
|
|
134
|
+
(!includeUnparsed &&
|
|
135
|
+
isSolTransferInstruction({ program: ix.program, type: ix.parsed.type })))
|
|
136
|
+
)
|
|
137
|
+
.map((ix) => {
|
|
138
|
+
let source = lodash.get(ix, 'parsed.info.source')
|
|
139
|
+
const destination = isSplMintInstruction({ program: ix.program, type: ix.parsed.type })
|
|
140
|
+
? lodash.get(ix, 'parsed.info.account') // only for minting
|
|
141
|
+
: lodash.get(ix, 'parsed.info.destination')
|
|
142
|
+
const amount = Number(
|
|
143
|
+
lodash.get(ix, 'parsed.info.amount', 0) ||
|
|
144
|
+
lodash.get(ix, 'parsed.info.tokenAmount.amount', 0)
|
|
145
|
+
)
|
|
146
|
+
const authority = lodash.get(ix, 'parsed.info.authority')
|
|
147
|
+
|
|
148
|
+
if (accountToRedeemToOwner && destination === accountToRedeemToOwner) {
|
|
149
|
+
solanaTransferTx = {
|
|
150
|
+
from: authority || source,
|
|
151
|
+
to: ownerAddress,
|
|
152
|
+
amount,
|
|
153
|
+
fee,
|
|
154
|
+
}
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
source === ownerAddress &&
|
|
160
|
+
isSolTransferInstruction({ program: ix.program, type: ix.parsed.type })
|
|
161
|
+
) {
|
|
162
|
+
const lamports = Number(lodash.get(ix, 'parsed.info.lamports', 0))
|
|
163
|
+
if (solanaTransferTx) {
|
|
164
|
+
solanaTransferTx.lamports += lamports
|
|
165
|
+
solanaTransferTx.amount = solanaTransferTx.lamports
|
|
166
|
+
if (!Array.isArray(solanaTransferTx.to)) {
|
|
167
|
+
solanaTransferTx.data = {
|
|
168
|
+
sent: [
|
|
169
|
+
{
|
|
170
|
+
address: solanaTransferTx.to,
|
|
171
|
+
amount: lamports,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
}
|
|
175
|
+
solanaTransferTx.to = [solanaTransferTx.to]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
solanaTransferTx.to.push(destination)
|
|
179
|
+
solanaTransferTx.data.sent.push({ address: destination, amount: lamports })
|
|
180
|
+
} else {
|
|
181
|
+
solanaTransferTx = {
|
|
182
|
+
source,
|
|
183
|
+
owner: source,
|
|
184
|
+
from: source,
|
|
185
|
+
to: [destination],
|
|
186
|
+
lamports,
|
|
187
|
+
amount: lamports,
|
|
188
|
+
data: {
|
|
189
|
+
sent: [
|
|
190
|
+
{
|
|
191
|
+
address: destination,
|
|
192
|
+
amount: lamports,
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
fee,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
204
|
+
return [source, destination].includes(tokenAccountAddress)
|
|
205
|
+
})
|
|
206
|
+
if (!tokenAccount) return
|
|
207
|
+
|
|
208
|
+
if (isSplMintInstruction({ program: ix.program, type: ix.parsed.type })) {
|
|
209
|
+
source = lodash.get(ix, 'parsed.info.mintAuthority')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isSending = tokenAccountsByOwner.some(({ tokenAccountAddress }) => {
|
|
213
|
+
return [source].includes(tokenAccountAddress)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// owner if it's a send tx
|
|
217
|
+
return {
|
|
218
|
+
id: txId,
|
|
219
|
+
program: ix.program,
|
|
220
|
+
type: ix.parsed.type,
|
|
221
|
+
slot: txDetails.slot,
|
|
222
|
+
owner: isSending ? ownerAddress : null,
|
|
223
|
+
from: isSending ? ownerAddress : source,
|
|
224
|
+
to: isSending ? destination : ownerAddress,
|
|
225
|
+
amount,
|
|
226
|
+
token: tokenAccount,
|
|
227
|
+
// Attribute fee only when owner is the actual fee payer
|
|
228
|
+
fee: isSending && ownerIsFeePayer ? fee : 0,
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
.filter((ix) => !!ix)
|
|
232
|
+
|
|
233
|
+
// Collect inner instructions into batch sends
|
|
234
|
+
for (let i = 0; i < innerInstructions.length - 1; i++) {
|
|
235
|
+
const tx = innerInstructions[i]
|
|
236
|
+
|
|
237
|
+
for (let j = i + 1; j < innerInstructions.length; j++) {
|
|
238
|
+
const next = innerInstructions[j]
|
|
239
|
+
if (
|
|
240
|
+
tx.id === next.id &&
|
|
241
|
+
tx.token === next.token &&
|
|
242
|
+
tx.owner === ownerAddress &&
|
|
243
|
+
tx.from === next.from
|
|
244
|
+
) {
|
|
245
|
+
if (!tx.data) {
|
|
246
|
+
tx.data = { sent: [{ address: tx.to, amount: tx.amount }] }
|
|
247
|
+
tx.to = [tx.to]
|
|
248
|
+
tx.fee = 0
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
tx.data.sent.push({
|
|
252
|
+
address: next.to,
|
|
253
|
+
amount: next.amount,
|
|
254
|
+
})
|
|
255
|
+
tx.to.push(next.to)
|
|
256
|
+
|
|
257
|
+
tx.amount += next.amount
|
|
258
|
+
|
|
259
|
+
innerInstructions.splice(j, 1)
|
|
260
|
+
j--
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// program:type tells us if it's a SOL or Token transfer
|
|
266
|
+
|
|
267
|
+
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
268
|
+
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
269
|
+
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
270
|
+
|
|
271
|
+
let tx = {}
|
|
272
|
+
if (stakeTx) {
|
|
273
|
+
// start staking
|
|
274
|
+
tx = {
|
|
275
|
+
owner: stakeTx.base,
|
|
276
|
+
from: stakeTx.base,
|
|
277
|
+
to: stakeTx.base,
|
|
278
|
+
amount: stakeTx.lamports,
|
|
279
|
+
fee,
|
|
280
|
+
staking: {
|
|
281
|
+
method: 'createAccountWithSeed',
|
|
282
|
+
seed: stakeTx.seed,
|
|
283
|
+
stakeAddresses: [stakeTx.newAccount],
|
|
284
|
+
stake: stakeTx.lamports,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
} else if (stakeWithdraw) {
|
|
288
|
+
const stakeAccounts = lodash.map(
|
|
289
|
+
lodash.filter(instructions, { program: 'stake', type: 'withdraw' }),
|
|
290
|
+
'stakeAccount'
|
|
291
|
+
)
|
|
292
|
+
tx = {
|
|
293
|
+
owner: stakeWithdraw.withdrawAuthority,
|
|
294
|
+
from: stakeWithdraw.stakeAccount,
|
|
295
|
+
to: stakeWithdraw.destination,
|
|
296
|
+
amount: stakeWithdraw.lamports,
|
|
297
|
+
fee,
|
|
298
|
+
staking: {
|
|
299
|
+
method: 'withdraw',
|
|
300
|
+
stakeAddresses: stakeAccounts,
|
|
301
|
+
stake: stakeWithdraw.lamports,
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
} else if (stakeUndelegate) {
|
|
305
|
+
const stakeAccounts = lodash.map(
|
|
306
|
+
lodash.filter(instructions, { program: 'stake', type: 'deactivate' }),
|
|
307
|
+
'stakeAccount'
|
|
308
|
+
)
|
|
309
|
+
tx = {
|
|
310
|
+
owner: stakeUndelegate.stakeAuthority,
|
|
311
|
+
from: stakeUndelegate.stakeAuthority,
|
|
312
|
+
to: stakeUndelegate.stakeAccount, // obsolete
|
|
313
|
+
amount: 0,
|
|
314
|
+
fee,
|
|
315
|
+
staking: {
|
|
316
|
+
method: 'undelegate',
|
|
317
|
+
stakeAddresses: stakeAccounts,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
if (solanaTransferTx) {
|
|
322
|
+
const isSending = ownerAddress === solanaTransferTx.source
|
|
323
|
+
tx = {
|
|
324
|
+
owner: solanaTransferTx.source,
|
|
325
|
+
from: solanaTransferTx.source,
|
|
326
|
+
to: solanaTransferTx.destination,
|
|
327
|
+
amount: solanaTransferTx.lamports, // number
|
|
328
|
+
fee: isSending && ownerIsFeePayer ? fee : 0,
|
|
329
|
+
data: solanaTransferTx.data,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const accountIndexes = accountKeys.reduce((acc, key, i) => {
|
|
334
|
+
const hasKnownOwner = tokenAccountsByOwner.some(
|
|
335
|
+
(tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
acc[i] = {
|
|
339
|
+
...key,
|
|
340
|
+
owner: hasKnownOwner ? ownerAddress : null, // not know (like in an outgoing tx)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return acc
|
|
344
|
+
}, Object.create(null)) // { 0: { pubkey, owner }, 1: { ... }, ... }
|
|
345
|
+
|
|
346
|
+
// Parse Token txs
|
|
347
|
+
const tokenTxs = _parseTokenTransfers({
|
|
348
|
+
instructions,
|
|
349
|
+
innerInstructions,
|
|
350
|
+
tokenAccountsByOwner,
|
|
351
|
+
ownerAddress,
|
|
352
|
+
fee: ownerIsFeePayer ? fee : 0,
|
|
353
|
+
accountIndexes,
|
|
354
|
+
preTokenBalances,
|
|
355
|
+
postTokenBalances,
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
if (tokenTxs.length > 0) {
|
|
359
|
+
// found spl-token simple transfer/transferChecked instruction
|
|
360
|
+
tx.tokenTxs = tokenTxs.map((tx) => ({
|
|
361
|
+
id: txId,
|
|
362
|
+
slot: txDetails.slot,
|
|
363
|
+
error: !(txDetails.meta.err === null),
|
|
364
|
+
...tx,
|
|
365
|
+
}))
|
|
366
|
+
} else if (preTokenBalances && postTokenBalances) {
|
|
367
|
+
// probably a DEX program is involved (multiple instructions), compute balance changes
|
|
368
|
+
// group by owner and supported token
|
|
369
|
+
const preBalances = preTokenBalances.filter((t) => {
|
|
370
|
+
return accountIndexes[t.accountIndex].owner === ownerAddress
|
|
371
|
+
})
|
|
372
|
+
const postBalances = postTokenBalances.filter((t) => {
|
|
373
|
+
return accountIndexes[t.accountIndex].owner === ownerAddress
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
if (preBalances.length > 0 || postBalances.length > 0 || solanaTransferTx) {
|
|
377
|
+
tx = {}
|
|
378
|
+
|
|
379
|
+
if (includeUnparsed && innerInstructions.length > 0) {
|
|
380
|
+
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
381
|
+
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
382
|
+
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
383
|
+
// SOL->SPL swaps on Raydium and Orca.
|
|
384
|
+
tx = getUnparsedTx()
|
|
385
|
+
tx.dexTxs = getInnerTxsFromBalanceChanges()
|
|
386
|
+
} else {
|
|
387
|
+
if (solanaTransferTx) {
|
|
388
|
+
// the base tx will be the one that moved solana.
|
|
389
|
+
tx =
|
|
390
|
+
solanaTransferTx.from && solanaTransferTx.to
|
|
391
|
+
? solanaTransferTx
|
|
392
|
+
: {
|
|
393
|
+
owner: solanaTransferTx.source,
|
|
394
|
+
from: solanaTransferTx.source,
|
|
395
|
+
to: solanaTransferTx.destination,
|
|
396
|
+
amount: solanaTransferTx.lamports, // number
|
|
397
|
+
fee: ownerAddress === solanaTransferTx.source ? fee : 0,
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
402
|
+
if (innerInstructions.length > 0) {
|
|
403
|
+
tx.dexTxs = innerInstructions
|
|
404
|
+
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
405
|
+
if (!tx.from && !solanaTransferTx) {
|
|
406
|
+
tx = tx.dexTxs[0]
|
|
407
|
+
tx.dexTxs = innerInstructions.slice(1)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const unparsed = Object.keys(tx).length === 0
|
|
416
|
+
|
|
417
|
+
if (unparsed && includeUnparsed) {
|
|
418
|
+
tx = getUnparsedTx()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// How tokens tx are parsed:
|
|
422
|
+
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
423
|
+
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
424
|
+
// 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
|
|
425
|
+
// QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
id: txId,
|
|
429
|
+
slot: txDetails.slot,
|
|
430
|
+
error: !(txDetails.meta.err === null),
|
|
431
|
+
...tx,
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function _parseTokenTransfers({
|
|
436
|
+
instructions,
|
|
437
|
+
innerInstructions = [],
|
|
438
|
+
tokenAccountsByOwner,
|
|
439
|
+
ownerAddress,
|
|
440
|
+
fee,
|
|
441
|
+
accountIndexes = {},
|
|
442
|
+
preTokenBalances,
|
|
443
|
+
postTokenBalances,
|
|
444
|
+
}) {
|
|
445
|
+
if (
|
|
446
|
+
preTokenBalances.length === 0 &&
|
|
447
|
+
postTokenBalances.length === 0 &&
|
|
448
|
+
!Array.isArray(tokenAccountsByOwner)
|
|
449
|
+
) {
|
|
450
|
+
return []
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const tokenTxs = []
|
|
454
|
+
|
|
455
|
+
instructions.forEach((instruction) => {
|
|
456
|
+
const { type, program, source, destination, amount, tokenAmount } = instruction
|
|
457
|
+
|
|
458
|
+
if (isSplTransferInstruction({ program, type })) {
|
|
459
|
+
let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: source })
|
|
460
|
+
const isSending = !!tokenAccount
|
|
461
|
+
if (!isSending) {
|
|
462
|
+
// receiving
|
|
463
|
+
tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
464
|
+
tokenAccountAddress: destination,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!tokenAccount) return // no transfers with our addresses involved
|
|
469
|
+
|
|
470
|
+
const owner = isSending ? ownerAddress : null
|
|
471
|
+
|
|
472
|
+
delete tokenAccount.balance
|
|
473
|
+
delete tokenAccount.owner
|
|
474
|
+
|
|
475
|
+
// If it's a sending tx we want to have the destination's owner as "to" address
|
|
476
|
+
let to = ownerAddress
|
|
477
|
+
let from = ownerAddress
|
|
478
|
+
if (isSending) {
|
|
479
|
+
to = destination // token account address (trying to get the owner below, we don't always have postTokenBalances...)
|
|
480
|
+
postTokenBalances.forEach((t) => {
|
|
481
|
+
if (accountIndexes[t.accountIndex].pubkey === destination) to = t.owner
|
|
482
|
+
})
|
|
483
|
+
} else {
|
|
484
|
+
// is receiving tx
|
|
485
|
+
from = source // token account address
|
|
486
|
+
preTokenBalances.forEach((t) => {
|
|
487
|
+
if (accountIndexes[t.accountIndex].pubkey === source) from = t.owner
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
tokenTxs.push({
|
|
492
|
+
owner,
|
|
493
|
+
token: tokenAccount,
|
|
494
|
+
from,
|
|
495
|
+
to,
|
|
496
|
+
amount: String(amount || tokenAmount?.amount || '0'), // supporting types: transfer, transferChecked, transferCheckedWithFee
|
|
497
|
+
fee: isSending ? fee : 0, // in lamports
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
innerInstructions.forEach((parsedIx) => {
|
|
503
|
+
const { type, program, amount, from, to } = parsedIx
|
|
504
|
+
|
|
505
|
+
// Handle token minting (mintTo, mintToChecked)
|
|
506
|
+
if (isSplMintInstruction({ program, type })) {
|
|
507
|
+
const {
|
|
508
|
+
token: { tokenAccountAddress },
|
|
509
|
+
} = parsedIx
|
|
510
|
+
|
|
511
|
+
// Check if the destination token account belongs to our owner
|
|
512
|
+
const tokenAccount = lodash.find(tokenAccountsByOwner, {
|
|
513
|
+
tokenAccountAddress,
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
if (!tokenAccount) return // not our token account
|
|
517
|
+
|
|
518
|
+
delete tokenAccount.balance
|
|
519
|
+
delete tokenAccount.owner
|
|
520
|
+
|
|
521
|
+
tokenTxs.push({
|
|
522
|
+
owner: null, // no owner for minting (it's created from thin air)
|
|
523
|
+
token: tokenAccount,
|
|
524
|
+
from, // mint address as the source
|
|
525
|
+
to, // our address as recipient
|
|
526
|
+
amount: String(amount || 0),
|
|
527
|
+
fee: 0, // no fee for receiving minted tokens
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
return tokenTxs
|
|
533
|
+
}
|