@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.
- package/package.json +3 -3
- 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
|
+
"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.
|
|
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": "
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
309
|
+
return (
|
|
310
|
+
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
311
|
+
)
|
|
305
312
|
})
|
|
306
313
|
const postBalances = postTokenBalances.filter((t) => {
|
|
307
|
-
return
|
|
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 {
|
|
319
|
-
|
|
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
|
|
365
|
-
|
|
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.
|
|
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
|
-
|
|
566
|
-
|
|
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()
|