@exodus/solana-api 1.4.7 → 2.0.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/package.json +2 -2
- package/src/index.js +112 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"node-fetch": "~1.6.3"
|
|
24
24
|
},
|
|
25
|
-
"gitHead": "
|
|
25
|
+
"gitHead": "d2b02ce96a053e1475edbcad3b9d304b0a2fbe92"
|
|
26
26
|
}
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,7 @@ class Api {
|
|
|
27
27
|
constructor(rpcUrl) {
|
|
28
28
|
this.setServer(rpcUrl)
|
|
29
29
|
this.setTokens(assets)
|
|
30
|
+
this.tokensToSkip = {}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
setServer(rpcUrl) {
|
|
@@ -120,7 +121,10 @@ class Api {
|
|
|
120
121
|
/**
|
|
121
122
|
* Get transactions from an address
|
|
122
123
|
*/
|
|
123
|
-
async getTransactions(
|
|
124
|
+
async getTransactions(
|
|
125
|
+
address: string,
|
|
126
|
+
{ cursor, before, limit, includeUnparsed = false } = {}
|
|
127
|
+
): any {
|
|
124
128
|
let transactions = []
|
|
125
129
|
// cursor is a txHash
|
|
126
130
|
|
|
@@ -151,8 +155,10 @@ class Api {
|
|
|
151
155
|
if (txDetail === null) return
|
|
152
156
|
|
|
153
157
|
const timestamp = txDetail.blockTime * 1000
|
|
154
|
-
const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner
|
|
155
|
-
|
|
158
|
+
const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner, {
|
|
159
|
+
includeUnparsed,
|
|
160
|
+
})
|
|
161
|
+
if (!parsedTx.from && !includeUnparsed) return // cannot parse it
|
|
156
162
|
|
|
157
163
|
// split dexTx in separate txs
|
|
158
164
|
if (parsedTx.dexTxs) {
|
|
@@ -184,12 +190,42 @@ class Api {
|
|
|
184
190
|
return { transactions, newCursor }
|
|
185
191
|
}
|
|
186
192
|
|
|
187
|
-
parseTransaction(
|
|
188
|
-
|
|
193
|
+
parseTransaction(
|
|
194
|
+
ownerAddress: string,
|
|
195
|
+
txDetails: Object,
|
|
196
|
+
tokenAccountsByOwner: ?Array,
|
|
197
|
+
{ includeUnparsed = false } = {}
|
|
198
|
+
): Object {
|
|
199
|
+
let {
|
|
200
|
+
fee,
|
|
201
|
+
preBalances,
|
|
202
|
+
postBalances,
|
|
203
|
+
preTokenBalances,
|
|
204
|
+
postTokenBalances,
|
|
205
|
+
innerInstructions,
|
|
206
|
+
} = txDetails.meta
|
|
207
|
+
preBalances = preBalances || []
|
|
208
|
+
postBalances = postBalances || []
|
|
189
209
|
preTokenBalances = preTokenBalances || []
|
|
190
210
|
postTokenBalances = postTokenBalances || []
|
|
191
211
|
innerInstructions = innerInstructions || []
|
|
212
|
+
|
|
192
213
|
let { instructions, accountKeys } = txDetails.transaction.message
|
|
214
|
+
|
|
215
|
+
const getUnparsedTx = () => {
|
|
216
|
+
const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
|
|
217
|
+
const feePaid = ownerIndex === 0 ? fee : 0
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
unparsed: true,
|
|
221
|
+
amount: postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
|
|
222
|
+
fee: feePaid,
|
|
223
|
+
data: {
|
|
224
|
+
meta: txDetails.meta,
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
193
229
|
instructions = instructions
|
|
194
230
|
.filter((ix) => ix.parsed) // only known instructions
|
|
195
231
|
.map((ix) => ({
|
|
@@ -368,30 +404,45 @@ class Api {
|
|
|
368
404
|
if (preBalances.length || postBalances.length) {
|
|
369
405
|
tx = {}
|
|
370
406
|
|
|
371
|
-
if (
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
407
|
+
if (includeUnparsed && innerInstructions.length) {
|
|
408
|
+
// when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
|
|
409
|
+
// 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
|
|
410
|
+
// 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
|
|
411
|
+
// SOL->SPL swaps on Raydium and Orca.
|
|
412
|
+
tx = getUnparsedTx(tx)
|
|
413
|
+
tx.dexTxs = innerInstructions.map((i) => ({ ...i, fee: 0 }))
|
|
414
|
+
} else {
|
|
415
|
+
if (solanaTx) {
|
|
416
|
+
// the base tx will be the one that moved solana.
|
|
417
|
+
tx = {
|
|
418
|
+
owner: solanaTx.source,
|
|
419
|
+
from: solanaTx.source,
|
|
420
|
+
to: solanaTx.destination,
|
|
421
|
+
amount: solanaTx.lamports, // number
|
|
422
|
+
fee: ownerAddress === solanaTx.source ? fee : 0,
|
|
423
|
+
}
|
|
379
424
|
}
|
|
380
|
-
}
|
|
381
425
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
426
|
+
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
427
|
+
if (innerInstructions.length) {
|
|
428
|
+
tx.dexTxs = innerInstructions
|
|
429
|
+
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
430
|
+
if (!tx.from && !solanaTx) {
|
|
431
|
+
tx = tx.dexTxs[0]
|
|
432
|
+
tx.dexTxs = innerInstructions.slice(1)
|
|
433
|
+
}
|
|
389
434
|
}
|
|
390
435
|
}
|
|
391
436
|
}
|
|
392
437
|
}
|
|
393
438
|
}
|
|
394
439
|
|
|
440
|
+
const unparsed = Object.keys(tx).length === 0
|
|
441
|
+
|
|
442
|
+
if (unparsed && includeUnparsed) {
|
|
443
|
+
tx = getUnparsedTx(tx)
|
|
444
|
+
}
|
|
445
|
+
|
|
395
446
|
// How tokens tx are parsed:
|
|
396
447
|
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
397
448
|
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
@@ -406,6 +457,36 @@ class Api {
|
|
|
406
457
|
}
|
|
407
458
|
}
|
|
408
459
|
|
|
460
|
+
async getSupply(mintAddress: string): string {
|
|
461
|
+
const { value: { amount } } = await this.api.post({
|
|
462
|
+
method: 'getTokenSupply',
|
|
463
|
+
params: [mintAddress],
|
|
464
|
+
})
|
|
465
|
+
return amount
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async getWalletTokensList({ address, tokenAccounts }) {
|
|
469
|
+
const tokensMint = []
|
|
470
|
+
for (let account of tokenAccounts) {
|
|
471
|
+
const mint = account.mintAddress
|
|
472
|
+
|
|
473
|
+
// skip cached NFT
|
|
474
|
+
if (this.tokensToSkip[mint]) continue
|
|
475
|
+
// skip 0 balance
|
|
476
|
+
if (account.balance === '0') continue
|
|
477
|
+
// skip NFT
|
|
478
|
+
const supply = await this.getSupply(mint)
|
|
479
|
+
if (supply === '1') {
|
|
480
|
+
this.tokensToSkip[mint] = true
|
|
481
|
+
continue
|
|
482
|
+
}
|
|
483
|
+
// OK
|
|
484
|
+
tokensMint.push(mint)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return tokensMint
|
|
488
|
+
}
|
|
489
|
+
|
|
409
490
|
async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
|
|
410
491
|
const { value: accountsList } = await this.api.post({
|
|
411
492
|
method: 'getTokenAccountsByOwner',
|
|
@@ -437,8 +518,8 @@ class Api {
|
|
|
437
518
|
: tokenAccounts
|
|
438
519
|
}
|
|
439
520
|
|
|
440
|
-
async getTokensBalance(address
|
|
441
|
-
let accounts = await this.getTokenAccountsByOwner(address)
|
|
521
|
+
async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
|
|
522
|
+
let accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
|
|
442
523
|
|
|
443
524
|
const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
|
|
444
525
|
if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
|
|
@@ -485,6 +566,11 @@ class Api {
|
|
|
485
566
|
return value
|
|
486
567
|
}
|
|
487
568
|
|
|
569
|
+
async isSpl(address: string) {
|
|
570
|
+
const { owner } = await this.getAccountInfo(address)
|
|
571
|
+
return owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
572
|
+
}
|
|
573
|
+
|
|
488
574
|
async getMetaplexMetadata(tokenMintAddress: string) {
|
|
489
575
|
const metaplexPDA = getMetadataAccount(tokenMintAddress)
|
|
490
576
|
const res = await this.getAccountInfo(metaplexPDA)
|
|
@@ -704,7 +790,9 @@ class Api {
|
|
|
704
790
|
*/
|
|
705
791
|
simulateAndRetrieveSideEffects = async (transactionMessage, publicKey: string) => {
|
|
706
792
|
const { config, accountAddresses } = getTransactionSimulationParams(transactionMessage)
|
|
707
|
-
const encodedTransaction = buildRawTransaction(transactionMessage.serialize()).toString(
|
|
793
|
+
const encodedTransaction = buildRawTransaction(transactionMessage.serialize()).toString(
|
|
794
|
+
'base64'
|
|
795
|
+
)
|
|
708
796
|
const futureAccountsState = await this.simulateTransaction(encodedTransaction, config)
|
|
709
797
|
const { solAccounts, tokenAccounts } = filterAccountsByOwner(
|
|
710
798
|
futureAccountsState,
|