@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.
Files changed (2) hide show
  1. package/package.json +2 -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": "1.4.7",
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": "7d0c7a92fe8f9d108f88584b6578edf7a66adfd4"
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(address: string, { cursor, before, limit } = {}): any {
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
- if (!parsedTx.from) return // cannot parse it
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(ownerAddress: string, txDetails: Object, tokenAccountsByOwner: ?Array): Object {
188
- let { fee, preTokenBalances, postTokenBalances, innerInstructions } = txDetails.meta
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 (solanaTx) {
372
- // the base tx will be the one that moved solana.
373
- tx = {
374
- owner: solanaTx.source,
375
- from: solanaTx.source,
376
- to: solanaTx.destination,
377
- amount: solanaTx.lamports, // number
378
- fee: ownerAddress === solanaTx.source ? fee : 0,
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
- // If it has inner instructions then it's a DEX tx that moved SPL -> SPL
383
- if (innerInstructions.length) {
384
- tx.dexTxs = innerInstructions
385
- // if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
386
- if (!tx.from && !solanaTx) {
387
- tx = tx.dexTxs[0]
388
- tx.dexTxs = innerInstructions.slice(1)
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: string, filterByTokens = []) {
441
- let accounts = await this.getTokenAccountsByOwner(address) // Tokens
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('base64')
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,