@exodus/solana-api 1.4.0 → 1.4.2
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 +93 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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.3.
|
|
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": "d7cd72173e6645069824b0669559481e3ff762c2"
|
|
26
26
|
}
|
package/src/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
deserializeMetaplexMetadata,
|
|
6
6
|
getTransactionSimulationParams,
|
|
7
7
|
filterAccountsByOwner,
|
|
8
|
-
tokens,
|
|
9
8
|
SYSTEM_PROGRAM_ID,
|
|
10
9
|
STAKE_PROGRAM_ID,
|
|
11
10
|
TOKEN_PROGRAM_ID,
|
|
12
11
|
SOL_DECIMAL,
|
|
13
12
|
computeBalance,
|
|
14
13
|
} from '@exodus/solana-lib'
|
|
14
|
+
import assets from '@exodus/assets'
|
|
15
15
|
import assert from 'assert'
|
|
16
16
|
import lodash from 'lodash'
|
|
17
17
|
import urljoin from 'url-join'
|
|
@@ -25,6 +25,7 @@ const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.sol
|
|
|
25
25
|
class Api {
|
|
26
26
|
constructor(rpcUrl) {
|
|
27
27
|
this.setServer(rpcUrl)
|
|
28
|
+
this.setTokens(assets)
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
setServer(rpcUrl) {
|
|
@@ -32,12 +33,21 @@ class Api {
|
|
|
32
33
|
this.api = createApi(this.rpcUrl)
|
|
33
34
|
}
|
|
34
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
|
+
|
|
35
41
|
request(path, contentType = 'application/json'): Wretcher {
|
|
36
42
|
return wretch(urljoin(this.rpcUrl, path)).headers({
|
|
37
43
|
'Content-type': contentType,
|
|
38
44
|
})
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
isTokenSupported(mint: string) {
|
|
48
|
+
return !!this.tokens[mint]
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
async getEpochInfo(): number {
|
|
42
52
|
const { epoch } = await this.api.post({
|
|
43
53
|
method: 'getEpochInfo',
|
|
@@ -140,9 +150,21 @@ class Api {
|
|
|
140
150
|
if (txDetail === null) return
|
|
141
151
|
|
|
142
152
|
const timestamp = txDetail.blockTime * 1000
|
|
143
|
-
const parsedTx =
|
|
153
|
+
const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner)
|
|
144
154
|
if (!parsedTx.from) return // cannot parse it
|
|
145
155
|
|
|
156
|
+
// split dexTx in separate txs
|
|
157
|
+
if (parsedTx.dexTxs) {
|
|
158
|
+
parsedTx.dexTxs.forEach((tx) => {
|
|
159
|
+
transactions.push({
|
|
160
|
+
timestamp,
|
|
161
|
+
date: new Date(timestamp),
|
|
162
|
+
...tx,
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
delete parsedTx.dexTxs
|
|
166
|
+
}
|
|
167
|
+
|
|
146
168
|
transactions.push({
|
|
147
169
|
timestamp,
|
|
148
170
|
date: new Date(timestamp),
|
|
@@ -161,17 +183,11 @@ class Api {
|
|
|
161
183
|
return { transactions, newCursor }
|
|
162
184
|
}
|
|
163
185
|
|
|
164
|
-
parseTransaction(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
static parseTransaction(
|
|
170
|
-
ownerAddress: string,
|
|
171
|
-
txDetails: Object,
|
|
172
|
-
tokenAccountsByOwner: ?Array
|
|
173
|
-
): Object {
|
|
174
|
-
const { fee, preTokenBalances, postTokenBalances } = txDetails.meta
|
|
186
|
+
parseTransaction(ownerAddress: string, txDetails: Object, tokenAccountsByOwner: ?Array): Object {
|
|
187
|
+
let { fee, preTokenBalances, postTokenBalances, innerInstructions } = txDetails.meta
|
|
188
|
+
preTokenBalances = preTokenBalances || []
|
|
189
|
+
postTokenBalances = postTokenBalances || []
|
|
190
|
+
innerInstructions = innerInstructions || []
|
|
175
191
|
let { instructions, accountKeys } = txDetails.transaction.message
|
|
176
192
|
instructions = instructions
|
|
177
193
|
.filter((ix) => ix.parsed) // only known instructions
|
|
@@ -180,6 +196,38 @@ class Api {
|
|
|
180
196
|
type: ix.parsed.type, // transfer, createAccount, initializeAccount
|
|
181
197
|
...ix.parsed.info,
|
|
182
198
|
}))
|
|
199
|
+
innerInstructions = innerInstructions
|
|
200
|
+
.reduce((acc, val) => {
|
|
201
|
+
return acc.concat(val.instructions)
|
|
202
|
+
}, [])
|
|
203
|
+
.map((ix) => {
|
|
204
|
+
const type = lodash.get(ix, 'parsed.type')
|
|
205
|
+
const isTransferTx = ix.parsed && ix.program === 'spl-token' && type === 'transfer'
|
|
206
|
+
const source = lodash.get(ix, 'parsed.info.source')
|
|
207
|
+
const destination = lodash.get(ix, 'parsed.info.destination')
|
|
208
|
+
const amount = Number(lodash.get(ix, 'parsed.info.amount', 0))
|
|
209
|
+
|
|
210
|
+
const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
211
|
+
return [source, destination].includes(tokenAccountAddress)
|
|
212
|
+
})
|
|
213
|
+
const isSending = !!tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
|
|
214
|
+
return [source].includes(tokenAccountAddress)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// owner if it's a send tx
|
|
218
|
+
const instruction = {
|
|
219
|
+
id: txDetails.transaction.signatures[0],
|
|
220
|
+
slot: txDetails.slot,
|
|
221
|
+
owner: isSending ? ownerAddress : null,
|
|
222
|
+
from: source,
|
|
223
|
+
to: destination,
|
|
224
|
+
amount,
|
|
225
|
+
token: tokenAccount,
|
|
226
|
+
fee: isSending ? fee : 0,
|
|
227
|
+
}
|
|
228
|
+
return isTransferTx && tokenAccount ? instruction : null
|
|
229
|
+
})
|
|
230
|
+
.filter((ix) => !!ix)
|
|
183
231
|
|
|
184
232
|
// program:type tells us if it's a SOL or Token transfer
|
|
185
233
|
const solanaTx = lodash.find(instructions, (ix) => {
|
|
@@ -189,9 +237,10 @@ class Api {
|
|
|
189
237
|
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
190
238
|
const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
|
|
191
239
|
const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
|
|
240
|
+
const hasSolanaTx = solanaTx && !preTokenBalances.length && !postTokenBalances.length // only SOL moved and no tokens movements
|
|
192
241
|
|
|
193
242
|
let tx = {}
|
|
194
|
-
if (
|
|
243
|
+
if (hasSolanaTx) {
|
|
195
244
|
// Solana tx
|
|
196
245
|
const isSending = ownerAddress === solanaTx.source
|
|
197
246
|
tx = {
|
|
@@ -305,36 +354,38 @@ class Api {
|
|
|
305
354
|
|
|
306
355
|
// group by owner and supported token
|
|
307
356
|
const preBalances = preTokenBalances.filter((t) => {
|
|
308
|
-
return
|
|
357
|
+
return (
|
|
358
|
+
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
359
|
+
)
|
|
309
360
|
})
|
|
310
361
|
const postBalances = postTokenBalances.filter((t) => {
|
|
311
|
-
return
|
|
362
|
+
return (
|
|
363
|
+
accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
|
|
364
|
+
)
|
|
312
365
|
})
|
|
313
366
|
|
|
314
|
-
if (preBalances.length
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
367
|
+
if (preBalances.length || postBalances.length) {
|
|
368
|
+
tx = {}
|
|
369
|
+
|
|
370
|
+
if (solanaTx) {
|
|
371
|
+
// the base tx will be the one that moved solana.
|
|
372
|
+
tx = {
|
|
373
|
+
owner: solanaTx.source,
|
|
374
|
+
from: solanaTx.source,
|
|
375
|
+
to: solanaTx.destination,
|
|
376
|
+
amount: solanaTx.lamports, // number
|
|
377
|
+
fee: ownerAddress === solanaTx.source ? fee : 0,
|
|
378
|
+
}
|
|
325
379
|
}
|
|
326
380
|
|
|
327
|
-
tx
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
to: ownerAddress, // DEX always interact with the owner address
|
|
336
|
-
amount,
|
|
337
|
-
fee: isSending ? fee : 0, // in lamports
|
|
381
|
+
// If it has inner instructions then it's a DEX tx that moved SPL -> SPL
|
|
382
|
+
if (innerInstructions.length) {
|
|
383
|
+
tx.dexTxs = innerInstructions
|
|
384
|
+
// if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
|
|
385
|
+
if (!tx.from && !solanaTx) {
|
|
386
|
+
tx = tx.dexTxs[0]
|
|
387
|
+
tx.dexTxs = innerInstructions.slice(1)
|
|
388
|
+
}
|
|
338
389
|
}
|
|
339
390
|
}
|
|
340
391
|
}
|
|
@@ -343,7 +394,7 @@ class Api {
|
|
|
343
394
|
// How tokens tx are parsed:
|
|
344
395
|
// 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
|
|
345
396
|
// 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
|
|
346
|
-
// 2. if it's an incoming tx:
|
|
397
|
+
// 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
|
|
347
398
|
// QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
|
|
348
399
|
|
|
349
400
|
return {
|
|
@@ -365,15 +416,15 @@ class Api {
|
|
|
365
416
|
const { pubkey, account } = entry
|
|
366
417
|
|
|
367
418
|
const mint = lodash.get(account, 'data.parsed.info.mint')
|
|
368
|
-
const token = tokens
|
|
369
|
-
|
|
419
|
+
const token = this.tokens[mint] || {
|
|
420
|
+
name: 'unknown',
|
|
370
421
|
ticker: 'UNKNOWN',
|
|
371
422
|
}
|
|
372
423
|
const balance = lodash.get(account, 'data.parsed.info.tokenAmount.amount', '0')
|
|
373
424
|
tokenAccounts.push({
|
|
374
425
|
tokenAccountAddress: pubkey,
|
|
375
426
|
owner: address,
|
|
376
|
-
tokenName: token.
|
|
427
|
+
tokenName: token.name,
|
|
377
428
|
ticker: token.ticker,
|
|
378
429
|
balance,
|
|
379
430
|
mintAddress: mint,
|
|
@@ -663,8 +714,4 @@ class Api {
|
|
|
663
714
|
}
|
|
664
715
|
}
|
|
665
716
|
|
|
666
|
-
function isTokenSupported(mint: string) {
|
|
667
|
-
return !!tokens.find(({ mintAddress }) => mintAddress === mint)
|
|
668
|
-
}
|
|
669
|
-
|
|
670
717
|
export default new Api()
|