@exodus/solana-api 1.3.3 → 1.4.1

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 +3 -3
  2. package/src/index.js +125 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@exodus/asset-json-rpc": "^1.0.0",
17
- "@exodus/solana-lib": "^1.2.29",
17
+ "@exodus/solana-lib": "^1.3.1",
18
18
  "lodash": "^4.17.11",
19
19
  "url-join": "4.0.0",
20
20
  "wretch": "^1.5.2"
@@ -22,5 +22,5 @@
22
22
  "devDependencies": {
23
23
  "node-fetch": "~1.6.3"
24
24
  },
25
- "gitHead": "69134d5324df0b3977a84b39e1098876a5ebffe7"
25
+ "gitHead": "49f3f78ac41acaf405e2849cccb2f9ecd0060424"
26
26
  }
package/src/index.js CHANGED
@@ -3,11 +3,15 @@ import createApi from '@exodus/asset-json-rpc'
3
3
  import {
4
4
  getMetadataAccount,
5
5
  deserializeMetaplexMetadata,
6
- tokens,
6
+ getTransactionSimulationParams,
7
+ filterAccountsByOwner,
7
8
  SYSTEM_PROGRAM_ID,
8
9
  STAKE_PROGRAM_ID,
9
10
  TOKEN_PROGRAM_ID,
11
+ SOL_DECIMAL,
12
+ computeBalance,
10
13
  } from '@exodus/solana-lib'
14
+ import assets from '@exodus/assets'
11
15
  import assert from 'assert'
12
16
  import lodash from 'lodash'
13
17
  import urljoin from 'url-join'
@@ -21,6 +25,7 @@ const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.sol
21
25
  class Api {
22
26
  constructor(rpcUrl) {
23
27
  this.setServer(rpcUrl)
28
+ this.setTokens(assets)
24
29
  }
25
30
 
26
31
  setServer(rpcUrl) {
@@ -28,12 +33,21 @@ class Api {
28
33
  this.api = createApi(this.rpcUrl)
29
34
  }
30
35
 
36
+ setTokens(assets = {}) {
37
+ const solTokens = lodash.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
38
+ this.tokens = lodash.mapKeys(solTokens, (v) => v.mintAddress )
39
+ }
40
+
31
41
  request(path, contentType = 'application/json'): Wretcher {
32
42
  return wretch(urljoin(this.rpcUrl, path)).headers({
33
43
  'Content-type': contentType,
34
44
  })
35
45
  }
36
46
 
47
+ isTokenSupported(mint: string) {
48
+ return !!this.tokens[mint]
49
+ }
50
+
37
51
  async getEpochInfo(): number {
38
52
  const { epoch } = await this.api.post({
39
53
  method: 'getEpochInfo',
@@ -136,7 +150,7 @@ class Api {
136
150
  if (txDetail === null) return
137
151
 
138
152
  const timestamp = txDetail.blockTime * 1000
139
- const parsedTx = Api.parseTransaction(address, txDetail, tokenAccountsByOwner)
153
+ const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner)
140
154
  if (!parsedTx.from) return // cannot parse it
141
155
 
142
156
  transactions.push({
@@ -157,16 +171,7 @@ class Api {
157
171
  return { transactions, newCursor }
158
172
  }
159
173
 
160
- parseTransaction(...args) {
161
- // alias
162
- return Api.parseTransaction(...args)
163
- }
164
-
165
- static parseTransaction(
166
- ownerAddress: string,
167
- txDetails: Object,
168
- tokenAccountsByOwner: ?Array
169
- ): Object {
174
+ parseTransaction(ownerAddress: string, txDetails: Object, tokenAccountsByOwner: ?Array): Object {
170
175
  const { fee, preTokenBalances, postTokenBalances } = txDetails.meta
171
176
  let { instructions, accountKeys } = txDetails.transaction.message
172
177
  instructions = instructions
@@ -301,10 +306,14 @@ class Api {
301
306
 
302
307
  // group by owner and supported token
303
308
  const preBalances = preTokenBalances.filter((t) => {
304
- return accountIndexes[t.accountIndex].owner === ownerAddress && isTokenSupported(t.mint)
309
+ return (
310
+ accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
311
+ )
305
312
  })
306
313
  const postBalances = postTokenBalances.filter((t) => {
307
- return accountIndexes[t.accountIndex].owner === ownerAddress && isTokenSupported(t.mint)
314
+ return (
315
+ accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
316
+ )
308
317
  })
309
318
 
310
319
  if (preBalances.length && preBalances.length === postBalances.length) {
@@ -315,8 +324,8 @@ class Api {
315
324
  const isSending = Math.sign(amount) <= 0
316
325
 
317
326
  const mint = lodash.get(postBalances, '[0].mint', '')
318
- const { tokenName, ticker } = tokens.find(({ mintAddress }) => mintAddress === mint) || {
319
- tokenName: 'unknown',
327
+ const { name, ticker } = this.tokens[mint] || {
328
+ name: 'unknown',
320
329
  ticker: 'UNKNOWN',
321
330
  }
322
331
 
@@ -324,7 +333,7 @@ class Api {
324
333
  owner: isSending ? ownerAddress : TOKEN_PROGRAM_ID.toBase58(),
325
334
  token: {
326
335
  tokenAccountAddress: '', // token account is closed after the swap
327
- tokenName,
336
+ tokenName: name,
328
337
  ticker,
329
338
  },
330
339
  from: isSending ? ownerAddress : TOKEN_PROGRAM_ID.toBase58(),
@@ -361,15 +370,15 @@ class Api {
361
370
  const { pubkey, account } = entry
362
371
 
363
372
  const mint = lodash.get(account, 'data.parsed.info.mint')
364
- const token = tokens.find(({ mintAddress }) => mintAddress === mint) || {
365
- tokenName: 'unknown',
373
+ const token = this.tokens[mint] || {
374
+ name: 'unknown',
366
375
  ticker: 'UNKNOWN',
367
376
  }
368
377
  const balance = lodash.get(account, 'data.parsed.info.tokenAmount.amount', '0')
369
378
  tokenAccounts.push({
370
379
  tokenAccountAddress: pubkey,
371
380
  owner: address,
372
- tokenName: token.tokenName,
381
+ tokenName: token.name,
373
382
  ticker: token.ticker,
374
383
  balance,
375
384
  mintAddress: mint,
@@ -409,6 +418,18 @@ class Api {
409
418
  }
410
419
  }
411
420
 
421
+ // Returns account balance of a SPL Token account.
422
+ async getTokenBalance(tokenAddress: string) {
423
+ const {
424
+ value: { amount },
425
+ } = await this.api.post({
426
+ method: 'getTokenAccountBalance',
427
+ params: [tokenAddress],
428
+ })
429
+
430
+ return amount
431
+ }
432
+
412
433
  async getAccountInfo(address: string) {
413
434
  const { value } = await this.api.post({
414
435
  method: 'getAccountInfo',
@@ -560,10 +581,91 @@ class Api {
560
581
  console.log(`tx ${JSON.stringify(result)} sent!`)
561
582
  return result || null
562
583
  }
563
- }
564
584
 
565
- function isTokenSupported(mint: string) {
566
- return !!tokens.find(({ mintAddress }) => mintAddress === mint)
585
+ simulateTransaction = async (transaction, options) => {
586
+ const {
587
+ value: { accounts },
588
+ } = await this.api.post({
589
+ method: 'simulateTransaction',
590
+ params: [transaction.serialize({ verifySignatures: false }).toString('base64'), options],
591
+ })
592
+
593
+ return accounts
594
+ }
595
+
596
+ resolveSimulationSideEffects = async (solAccounts, tokenAccounts) => {
597
+ const willReceive = []
598
+ const willSend = []
599
+
600
+ const resolveSols = solAccounts.map(async (account) => {
601
+ const currentAmount = await this.getBalance(account.address)
602
+ const balance = computeBalance(account.amount, currentAmount)
603
+ return {
604
+ name: 'SOL',
605
+ symbol: 'SOL',
606
+ balance,
607
+ decimal: SOL_DECIMAL,
608
+ type: 'SOL',
609
+ }
610
+ })
611
+ const _getTokenBalance = async (address) => {
612
+ try {
613
+ return await this.getTokenBalance(address)
614
+ } catch (error) {
615
+ if (error.message && error.message.includes('could not find account')) {
616
+ return '0'
617
+ }
618
+ throw error
619
+ }
620
+ }
621
+
622
+ const resolveTokens = tokenAccounts.map(async (account) => {
623
+ const [_tokenMetaPlex, currentAmount, decimal] = await Promise.all([
624
+ this.getMetaplexMetadata(account.mint),
625
+ _getTokenBalance(account.address),
626
+ this.getDecimals(account.mint),
627
+ ])
628
+
629
+ const balance = computeBalance(account.amount, currentAmount)
630
+ const tokenMetaPlex = _tokenMetaPlex || { name: null, symbol: null }
631
+ return {
632
+ name: tokenMetaPlex.name,
633
+ symbol: tokenMetaPlex.symbol,
634
+ balance,
635
+ decimal,
636
+ type: 'TOKEN',
637
+ }
638
+ })
639
+
640
+ const accounts = await Promise.all([...resolveSols, ...resolveTokens])
641
+ accounts.forEach((account) => {
642
+ if (account.balance > 0) {
643
+ willReceive.push(account)
644
+ } else {
645
+ willSend.push(account)
646
+ }
647
+ })
648
+
649
+ return {
650
+ willReceive,
651
+ willSend,
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Simulate transaction and return side effects
657
+ */
658
+ simulateAndRetrieveSideEffects = async (transaction, publicKey: string) => {
659
+ const { config, accountAddresses } = getTransactionSimulationParams(transaction)
660
+ const futureAccountsState = await this.simulateTransaction(transaction, config)
661
+ const { solAccounts, tokenAccounts } = filterAccountsByOwner(
662
+ futureAccountsState,
663
+ accountAddresses,
664
+ publicKey
665
+ )
666
+
667
+ return this.resolveSimulationSideEffects(solAccounts, tokenAccounts)
668
+ }
567
669
  }
568
670
 
569
671
  export default new Api()