@exodus/solana-api 2.0.4 → 2.0.6

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/src/index.js CHANGED
@@ -1,863 +1,9 @@
1
- // @flow
2
- import createApi from '@exodus/asset-json-rpc'
3
- import {
4
- getMetadataAccount,
5
- deserializeMetaplexMetadata,
6
- getTransactionSimulationParams,
7
- filterAccountsByOwner,
8
- SYSTEM_PROGRAM_ID,
9
- STAKE_PROGRAM_ID,
10
- TOKEN_PROGRAM_ID,
11
- SOL_DECIMAL,
12
- computeBalance,
13
- SolanaWeb3Message,
14
- buildRawTransaction,
15
- } from '@exodus/solana-lib'
16
- import assets from '@exodus/assets'
17
- import assert from 'assert'
18
- import lodash from 'lodash'
19
- import urljoin from 'url-join'
20
- import wretch, { Wretcher } from 'wretch'
21
- import { magicEden } from '@exodus/nfts-core'
22
-
23
- // Doc: https://docs.solana.com/apps/jsonrpc-api
24
-
25
- const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com, https://solana-api.projectserum.com
26
-
27
- // Tokens + SOL api support
28
- export class Api {
29
- constructor(rpcUrl) {
30
- this.setServer(rpcUrl)
31
- this.setTokens(assets)
32
- this.tokensToSkip = {}
33
- }
34
-
35
- setServer(rpcUrl) {
36
- this.rpcUrl = rpcUrl || RPC_URL
37
- this.api = createApi(this.rpcUrl)
38
- }
39
-
40
- setTokens(assets = {}) {
41
- const solTokens = lodash.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
42
- this.tokens = lodash.mapKeys(solTokens, (v) => v.mintAddress)
43
- }
44
-
45
- request(path, contentType = 'application/json'): Wretcher {
46
- return wretch(urljoin(this.rpcUrl, path)).headers({
47
- 'Content-type': contentType,
48
- })
49
- }
50
-
51
- isTokenSupported(mint: string) {
52
- return !!this.tokens[mint]
53
- }
54
-
55
- async getEpochInfo(): number {
56
- const { epoch } = await this.api.post({
57
- method: 'getEpochInfo',
58
- })
59
- return Number(epoch)
60
- }
61
-
62
- async getStakeActivation(address): string {
63
- const { state } = await this.api.post({
64
- method: 'getStakeActivation',
65
- params: [address],
66
- })
67
- return state
68
- }
69
-
70
- async getRecentBlockHash(): string {
71
- const {
72
- value: { blockhash },
73
- } = await this.api.post({
74
- method: 'getRecentBlockhash',
75
- })
76
- return blockhash
77
- }
78
-
79
- // Transaction structure: https://docs.solana.com/apps/jsonrpc-api#transaction-structure
80
- async getTransactionById(id: string) {
81
- const result = await this.api.post({
82
- method: 'getConfirmedTransaction',
83
- params: [id, 'jsonParsed'],
84
- })
85
- return result
86
- }
87
-
88
- async getFee(): number {
89
- const {
90
- value: {
91
- feeCalculator: { lamportsPerSignature },
92
- },
93
- } = await this.api.post({
94
- method: 'getRecentBlockhash',
95
- })
96
- return lamportsPerSignature
97
- }
98
-
99
- async getBalance(address: string): number {
100
- const res = await this.api.post({
101
- method: 'getBalance',
102
- params: [address],
103
- })
104
- return res.value || 0
105
- }
106
-
107
- async getBlockTime(slot: number) {
108
- // might result in error if executed on a validator with partial ledger (https://github.com/solana-labs/solana/issues/12413)
109
- return this.api.post({
110
- method: 'getBlockTime',
111
- params: [slot],
112
- })
113
- }
114
-
115
- async getConfirmedSignaturesForAddress(address: string, { until, before, limit } = {}): any {
116
- until = until || undefined
117
- return this.api.post({
118
- method: 'getSignaturesForAddress',
119
- params: [address, { until, before, limit }],
120
- })
121
- }
122
-
123
- /**
124
- * Get transactions from an address
125
- */
126
- async getTransactions(
127
- address: string,
128
- { cursor, before, limit, includeUnparsed = false } = {}
129
- ): any {
130
- let transactions = []
131
- // cursor is a txHash
132
-
133
- try {
134
- let until = cursor
135
-
136
- const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
137
- const tokenAccountAddresses = tokenAccountsByOwner
138
- .filter(({ tokenName }) => tokenName !== 'unknown')
139
- .map(({ tokenAccountAddress }) => tokenAccountAddress)
140
- const accountsToCheck = [address, ...tokenAccountAddresses]
141
-
142
- const txsResultsByAccount = await Promise.all(
143
- accountsToCheck.map((addr) =>
144
- this.getConfirmedSignaturesForAddress(addr, {
145
- until,
146
- before,
147
- limit,
148
- })
149
- )
150
- )
151
- let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []) // merge arrays
152
- txsId = lodash.uniqBy(txsId, 'signature')
153
-
154
- // get txs details in parallel
155
- const txsDetails = await Promise.all(txsId.map((tx) => this.getTransactionById(tx.signature)))
156
- txsDetails.forEach((txDetail) => {
157
- if (txDetail === null) return
158
-
159
- const timestamp = txDetail.blockTime * 1000
160
- const parsedTx = this.parseTransaction(address, txDetail, tokenAccountsByOwner, {
161
- includeUnparsed,
162
- })
163
- if (!parsedTx.from && !includeUnparsed) return // cannot parse it
164
-
165
- // split dexTx in separate txs
166
- if (parsedTx.dexTxs) {
167
- parsedTx.dexTxs.forEach((tx) => {
168
- transactions.push({
169
- timestamp,
170
- date: new Date(timestamp),
171
- ...tx,
172
- })
173
- })
174
- delete parsedTx.dexTxs
175
- }
176
-
177
- transactions.push({
178
- timestamp,
179
- date: new Date(timestamp),
180
- ...parsedTx,
181
- })
182
- })
183
- } catch (err) {
184
- console.warn('Solana error:', err)
185
- throw err
186
- }
187
-
188
- transactions = lodash.orderBy(transactions, ['timestamp'], ['desc'])
189
-
190
- const newCursor = transactions[0] ? transactions[0].id : cursor
191
-
192
- return { transactions, newCursor }
193
- }
194
-
195
- parseTransaction(
196
- ownerAddress: string,
197
- txDetails: Object,
198
- tokenAccountsByOwner: ?Array,
199
- { includeUnparsed = false } = {}
200
- ): Object {
201
- let {
202
- fee,
203
- preBalances,
204
- postBalances,
205
- preTokenBalances,
206
- postTokenBalances,
207
- innerInstructions,
208
- } = txDetails.meta
209
- preBalances = preBalances || []
210
- postBalances = postBalances || []
211
- preTokenBalances = preTokenBalances || []
212
- postTokenBalances = postTokenBalances || []
213
- innerInstructions = innerInstructions || []
214
-
215
- let { instructions, accountKeys } = txDetails.transaction.message
216
-
217
- const getUnparsedTx = () => {
218
- const ownerIndex = accountKeys.findIndex((accountKey) => accountKey.pubkey === ownerAddress)
219
- const feePaid = ownerIndex === 0 ? fee : 0
220
-
221
- return {
222
- unparsed: true,
223
- amount: postBalances[ownerIndex] - preBalances[ownerIndex] + feePaid,
224
- fee: feePaid,
225
- data: {
226
- meta: txDetails.meta,
227
- },
228
- }
229
- }
230
-
231
- instructions = instructions
232
- .filter((ix) => ix.parsed) // only known instructions
233
- .map((ix) => ({
234
- program: ix.program, // system or spl-token
235
- type: ix.parsed.type, // transfer, createAccount, initializeAccount
236
- ...ix.parsed.info,
237
- }))
238
- innerInstructions = innerInstructions
239
- .reduce((acc, val) => {
240
- return acc.concat(val.instructions)
241
- }, [])
242
- .map((ix) => {
243
- const type = lodash.get(ix, 'parsed.type')
244
- const isTransferTx = ix.parsed && ix.program === 'spl-token' && type === 'transfer'
245
- const source = lodash.get(ix, 'parsed.info.source')
246
- const destination = lodash.get(ix, 'parsed.info.destination')
247
- const amount = Number(lodash.get(ix, 'parsed.info.amount', 0))
248
-
249
- const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
250
- return [source, destination].includes(tokenAccountAddress)
251
- })
252
- const isSending = !!tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
253
- return [source].includes(tokenAccountAddress)
254
- })
255
-
256
- // owner if it's a send tx
257
- const instruction = {
258
- id: txDetails.transaction.signatures[0],
259
- slot: txDetails.slot,
260
- owner: isSending ? ownerAddress : null,
261
- from: source,
262
- to: destination,
263
- amount,
264
- token: tokenAccount,
265
- fee: isSending ? fee : 0,
266
- }
267
- return isTransferTx && tokenAccount ? instruction : null
268
- })
269
- .filter((ix) => !!ix)
270
-
271
- // program:type tells us if it's a SOL or Token transfer
272
- const solanaTx = lodash.find(instructions, (ix) => {
273
- if (![ix.source, ix.destination].includes(ownerAddress)) return false
274
- return ix.program === 'system' && ix.type === 'transfer'
275
- }) // get SOL transfer
276
- const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
277
- const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
278
- const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
279
- const hasSolanaTx = solanaTx && !preTokenBalances.length && !postTokenBalances.length // only SOL moved and no tokens movements
280
-
281
- let tx = {}
282
- if (hasSolanaTx) {
283
- // Solana tx
284
- const isSending = ownerAddress === solanaTx.source
285
- tx = {
286
- owner: solanaTx.source,
287
- from: solanaTx.source,
288
- to: solanaTx.destination,
289
- amount: solanaTx.lamports, // number
290
- fee: isSending ? fee : 0,
291
- }
292
- } else if (stakeTx) {
293
- // start staking
294
- tx = {
295
- owner: stakeTx.base,
296
- from: stakeTx.base,
297
- to: stakeTx.base,
298
- amount: stakeTx.lamports,
299
- fee,
300
- staking: {
301
- method: 'createAccountWithSeed',
302
- seed: stakeTx.seed,
303
- stakeAddresses: [stakeTx.newAccount],
304
- stake: stakeTx.lamports,
305
- },
306
- }
307
- } else if (stakeWithdraw) {
308
- const stakeAccounts = lodash.map(
309
- lodash.filter(instructions, { program: 'stake', type: 'withdraw' }),
310
- 'stakeAccount'
311
- )
312
- tx = {
313
- owner: stakeWithdraw.withdrawAuthority,
314
- from: stakeWithdraw.stakeAccount,
315
- to: stakeWithdraw.destination,
316
- amount: stakeWithdraw.lamports,
317
- fee,
318
- staking: {
319
- method: 'withdraw',
320
- stakeAddresses: stakeAccounts,
321
- stake: stakeWithdraw.lamports,
322
- },
323
- }
324
- } else if (stakeUndelegate) {
325
- const stakeAccounts = lodash.map(
326
- lodash.filter(instructions, { program: 'stake', type: 'deactivate' }),
327
- 'stakeAccount'
328
- )
329
- tx = {
330
- owner: stakeUndelegate.stakeAuthority,
331
- from: stakeUndelegate.stakeAuthority,
332
- to: stakeUndelegate.stakeAccount, // obsolete
333
- amount: 0,
334
- fee,
335
- staking: {
336
- method: 'undelegate',
337
- stakeAddresses: stakeAccounts,
338
- },
339
- }
340
- } else {
341
- // Token tx
342
- assert.ok(
343
- Array.isArray(tokenAccountsByOwner),
344
- 'tokenAccountsByOwner is required when parsing token tx'
345
- )
346
- let tokenTxs = lodash
347
- .filter(instructions, ({ program, type }) => {
348
- return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
349
- }) // get Token transfer: could have more than 1 instructions
350
- .map((ix) => {
351
- // add token details based on source/destination address
352
- let tokenAccount = lodash.find(tokenAccountsByOwner, { tokenAccountAddress: ix.source })
353
- const isSending = !!tokenAccount
354
- if (!isSending)
355
- tokenAccount = lodash.find(tokenAccountsByOwner, {
356
- tokenAccountAddress: ix.destination,
357
- }) // receiving
358
- if (!tokenAccount) return null // no transfers with our addresses involved
359
- const owner = isSending ? ownerAddress : null
360
-
361
- delete tokenAccount.balance
362
- delete tokenAccount.owner
363
- return {
364
- owner,
365
- token: tokenAccount,
366
- from: ix.source,
367
- to: ix.destination,
368
- amount: Number(ix.amount || lodash.get(ix, 'tokenAmount.amount', 0)), // supporting both types: transfer and transferChecked
369
- fee: isSending ? fee : 0, // in lamports
370
- }
371
- })
372
-
373
- if (tokenTxs.length) {
374
- // found spl-token simple transfer/transferChecked instruction
375
- // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
376
- tx = tokenTxs.reduce((finalTx, ix) => {
377
- if (!ix) return finalTx // skip null instructions
378
- if (!finalTx.token) return ix // init finalTx (support just 1 token type per tx)
379
- if (finalTx.token.ticker === ix.token.ticker) finalTx.amount += ix.amount
380
- return finalTx
381
- }, {})
382
- } else if (preTokenBalances && postTokenBalances) {
383
- // probably a DEX program is involved (multiple instructions), compute balance changes
384
-
385
- const accountIndexes = lodash.mapKeys(accountKeys, (x, i) => i)
386
- Object.values(accountIndexes).forEach((acc) => {
387
- // filter by ownerAddress
388
- const hasKnownOwner = !!lodash.find(tokenAccountsByOwner, {
389
- tokenAccountAddress: acc.pubkey,
390
- })
391
- acc.owner = hasKnownOwner ? ownerAddress : null
392
- })
393
-
394
- // group by owner and supported token
395
- const preBalances = preTokenBalances.filter((t) => {
396
- return (
397
- accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
398
- )
399
- })
400
- const postBalances = postTokenBalances.filter((t) => {
401
- return (
402
- accountIndexes[t.accountIndex].owner === ownerAddress && this.isTokenSupported(t.mint)
403
- )
404
- })
405
-
406
- if (preBalances.length || postBalances.length) {
407
- tx = {}
408
-
409
- if (includeUnparsed && innerInstructions.length) {
410
- // when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
411
- // 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
412
- // 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
413
- // SOL->SPL swaps on Raydium and Orca.
414
- tx = getUnparsedTx(tx)
415
- tx.dexTxs = innerInstructions.map((i) => ({ ...i, fee: 0 }))
416
- } else {
417
- if (solanaTx) {
418
- // the base tx will be the one that moved solana.
419
- tx = {
420
- owner: solanaTx.source,
421
- from: solanaTx.source,
422
- to: solanaTx.destination,
423
- amount: solanaTx.lamports, // number
424
- fee: ownerAddress === solanaTx.source ? fee : 0,
425
- }
426
- }
427
-
428
- // If it has inner instructions then it's a DEX tx that moved SPL -> SPL
429
- if (innerInstructions.length) {
430
- tx.dexTxs = innerInstructions
431
- // if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
432
- if (!tx.from && !solanaTx) {
433
- tx = tx.dexTxs[0]
434
- tx.dexTxs = innerInstructions.slice(1)
435
- }
436
- }
437
- }
438
- }
439
- }
440
- }
441
-
442
- const unparsed = Object.keys(tx).length === 0
443
-
444
- if (unparsed && includeUnparsed) {
445
- tx = getUnparsedTx(tx)
446
- }
447
-
448
- // How tokens tx are parsed:
449
- // 0. compute incoming or outgoing tx: it's outgoing if spl-token:transfer has source/destination included in tokenAccountsByOwner
450
- // 1. if it's a sent tx: sum all instructions amount (spl-token:transfer)
451
- // 2. if it's an incoming tx: sum all the amounts with destination included in tokenAccountsByOwner (aggregating by ticker)
452
- // QUESTION: How do I know what are my tokens addresses deterministically? It's not possible, gotta use tokenAccountsByOwner
453
-
454
- return {
455
- id: txDetails.transaction.signatures[0],
456
- slot: txDetails.slot,
457
- error: !(txDetails.meta.err === null),
458
- ...tx,
459
- }
460
- }
461
-
462
- async getSupply(mintAddress: string): string {
463
- const {
464
- value: { amount },
465
- } = await this.api.post({
466
- method: 'getTokenSupply',
467
- params: [mintAddress],
468
- })
469
- return amount
470
- }
471
-
472
- async getWalletTokensList({ address, tokenAccounts }) {
473
- const tokensMint = []
474
- for (let account of tokenAccounts) {
475
- const mint = account.mintAddress
476
-
477
- // skip cached NFT
478
- if (this.tokensToSkip[mint]) continue
479
- // skip 0 balance
480
- if (account.balance === '0') continue
481
- // skip NFT
482
- const supply = await this.getSupply(mint)
483
- if (supply === '1') {
484
- this.tokensToSkip[mint] = true
485
- continue
486
- }
487
- // OK
488
- tokensMint.push(mint)
489
- }
490
-
491
- return tokensMint
492
- }
493
-
494
- async getTokenAccountsByOwner(address: string, tokenTicker: ?string): Array {
495
- const { value: accountsList } = await this.api.post({
496
- method: 'getTokenAccountsByOwner',
497
- params: [address, { programId: TOKEN_PROGRAM_ID.toBase58() }, { encoding: 'jsonParsed' }],
498
- })
499
-
500
- const tokenAccounts = []
501
- for (let entry of accountsList) {
502
- const { pubkey, account } = entry
503
-
504
- const mint = lodash.get(account, 'data.parsed.info.mint')
505
- const token = this.tokens[mint] || {
506
- name: 'unknown',
507
- ticker: 'UNKNOWN',
508
- }
509
- const balance = lodash.get(account, 'data.parsed.info.tokenAmount.amount', '0')
510
- tokenAccounts.push({
511
- tokenAccountAddress: pubkey,
512
- owner: address,
513
- tokenName: token.name,
514
- ticker: token.ticker,
515
- balance,
516
- mintAddress: mint,
517
- })
518
- }
519
- // eventually filter by token
520
- return tokenTicker
521
- ? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
522
- : tokenAccounts
523
- }
524
-
525
- async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
526
- let accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
527
-
528
- const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
529
- if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
530
- return acc // filter by supported tokens only
531
- if (!acc[tokenName]) acc[tokenName] = Number(balance)
532
- // e.g { 'serum': 123 }
533
- else acc[tokenName] += Number(balance) // merge same token account balance
534
- return acc
535
- }, {})
536
-
537
- return tokensBalance
538
- }
539
-
540
- async isAssociatedTokenAccountActive(tokenAddress: string) {
541
- // Returns the token balance of an SPL Token account.
542
- try {
543
- await this.api.post({
544
- method: 'getTokenAccountBalance',
545
- params: [tokenAddress],
546
- })
547
- return true
548
- } catch (e) {
549
- return false
550
- }
551
- }
552
-
553
- // Returns account balance of a SPL Token account.
554
- async getTokenBalance(tokenAddress: string) {
555
- const {
556
- value: { amount },
557
- } = await this.api.post({
558
- method: 'getTokenAccountBalance',
559
- params: [tokenAddress],
560
- })
561
-
562
- return amount
563
- }
564
-
565
- async getAccountInfo(address: string) {
566
- const { value } = await this.api.post({
567
- method: 'getAccountInfo',
568
- params: [address, { encoding: 'jsonParsed', commitment: 'single' }],
569
- })
570
- return value
571
- }
572
-
573
- async isSpl(address: string) {
574
- const { owner } = await this.getAccountInfo(address)
575
- return owner === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
576
- }
577
-
578
- async getMetaplexMetadata(tokenMintAddress: string) {
579
- const metaplexPDA = getMetadataAccount(tokenMintAddress)
580
- const res = await this.getAccountInfo(metaplexPDA)
581
- const data = lodash.get(res, 'data[0]')
582
- if (!data) return null
583
-
584
- return deserializeMetaplexMetadata(Buffer.from(data, 'base64'))
585
- }
586
-
587
- async getDecimals(tokenMintAddress: string) {
588
- const res = await this.api.post({ method: 'getTokenSupply', params: [tokenMintAddress] })
589
- return lodash.get(res, 'value.decimals', null)
590
- }
591
-
592
- async getAddressType(address: string) {
593
- // solana, token or null (unknown), meaning address has never been initialized
594
- const value = await this.getAccountInfo(address)
595
- if (value === null) return null
596
-
597
- const account = {
598
- executable: value.executable,
599
- owner: value.owner,
600
- lamports: value.lamports,
601
- }
602
-
603
- return account.owner === SYSTEM_PROGRAM_ID.toBase58()
604
- ? 'solana'
605
- : account.owner === TOKEN_PROGRAM_ID.toBase58()
606
- ? 'token'
607
- : null
608
- }
609
-
610
- async getTokenAddressOwner(address: string) {
611
- const value = await this.getAccountInfo(address)
612
- const owner = lodash.get(value, 'data.parsed.info.owner', null)
613
- return owner
614
- }
615
-
616
- async getAddressMint(address) {
617
- const value = await this.getAccountInfo(address)
618
- const mintAddress = lodash.get(value, 'data.parsed.info.mint', null) // token mint
619
- return mintAddress
620
- }
621
-
622
- async isTokenAddress(address: string) {
623
- const type = await this.getAddressType(address)
624
- return type === 'token'
625
- }
626
-
627
- async isSOLaddress(address: string) {
628
- const type = await this.getAddressType(address)
629
- return type === 'solana'
630
- }
631
-
632
- async getStakeAccountsInfo(address: string) {
633
- // get staked amount and other info
634
- const res = await this.api.post({
635
- method: 'getProgramAccounts',
636
- params: [
637
- STAKE_PROGRAM_ID.toBase58(),
638
- {
639
- filters: [
640
- {
641
- memcmp: {
642
- offset: 12,
643
- bytes: address,
644
- },
645
- },
646
- ],
647
- encoding: 'jsonParsed',
648
- },
649
- ],
650
- })
651
- const accounts = {}
652
- let totalStake = 0
653
- let locked = 0
654
- let withdrawable = 0
655
- let pending = 0
656
- for (let entry of res) {
657
- const addr = entry.pubkey
658
- const lamports = lodash.get(entry, 'account.lamports', 0)
659
- const delegation = lodash.get(entry, 'account.data.parsed.info.stake.delegation', {})
660
- // could have no delegation if the created stake address did not perform a delegate transaction
661
-
662
- accounts[addr] = delegation
663
- accounts[addr].lamports = lamports // sol balance
664
- accounts[addr].activationEpoch = Number(accounts[addr].activationEpoch) || 0
665
- accounts[addr].deactivationEpoch = Number(accounts[addr].deactivationEpoch) || 0
666
- let state = 'inactive'
667
- if (delegation.activationEpoch) state = await this.getStakeActivation(addr)
668
- accounts[addr].state = state
669
- accounts[addr].isDeactivating = state === 'deactivating'
670
- accounts[addr].canWithdraw = state === 'inactive'
671
- accounts[addr].stake = Number(accounts[addr].stake) || 0 // active staked amount
672
- totalStake += accounts[addr].stake
673
- locked += ['active', 'activating'].includes(accounts[addr].state) ? lamports : 0
674
- withdrawable += accounts[addr].canWithdraw ? lamports : 0
675
- pending += accounts[addr].isDeactivating ? lamports : 0
676
- }
677
- return { accounts, totalStake, locked, withdrawable, pending }
678
- }
679
-
680
- async getRewards(stakingAddresses = []) {
681
- if (!stakingAddresses.length) return 0
682
-
683
- // custom endpoint!
684
- const rewards = await this.request(`rewards?addresses=${stakingAddresses.join(',')}`)
685
- .get()
686
- .error(500, () => ({})) // addresses not found
687
- .error(400, () => ({}))
688
- .json()
689
-
690
- // sum rewards for all addresses
691
- const earnings = Object.values(rewards).reduce((total, x) => {
692
- return total + x
693
- }, 0)
694
-
695
- return earnings
696
- }
697
-
698
- async getMinimumBalanceForRentExemption(size: number) {
699
- const minimumBalance = await this.api.post({
700
- method: 'getMinimumBalanceForRentExemption',
701
- params: [size],
702
- })
703
-
704
- return minimumBalance
705
- }
706
-
707
- /**
708
- * Broadcast a signed transaction
709
- */
710
- broadcastTransaction = async (signedTx: string): string => {
711
- console.log('Solana broadcasting TX:', signedTx) // base64
712
-
713
- const result = await this.api.post({
714
- method: 'sendTransaction',
715
- params: [signedTx, { encoding: 'base64', commitment: 'singleGossip' }],
716
- })
717
-
718
- console.log(`tx ${JSON.stringify(result)} sent!`)
719
- return result || null
720
- }
721
-
722
- simulateTransaction = async (encodedTransaction, options) => {
723
- const {
724
- value: { accounts },
725
- } = await this.api.post({
726
- method: 'simulateTransaction',
727
- params: [encodedTransaction, options],
728
- })
729
-
730
- return accounts
731
- }
732
-
733
- resolveSimulationSideEffects = async (solAccounts, tokenAccounts) => {
734
- const willReceive = []
735
- const willSend = []
736
-
737
- const resolveSols = solAccounts.map(async (account) => {
738
- const currentAmount = await this.getBalance(account.address)
739
- const balance = computeBalance(account.amount, currentAmount)
740
- return {
741
- name: 'SOL',
742
- symbol: 'SOL',
743
- balance,
744
- decimal: SOL_DECIMAL,
745
- type: 'SOL',
746
- }
747
- })
748
-
749
- const _wrapAndHandleAccountNotFound = (fn, defaultValue) => {
750
- return async (...params) => {
751
- try {
752
- return await fn.apply(this, params)
753
- } catch (error) {
754
- if (error.message && error.message.includes('could not find account')) {
755
- return defaultValue
756
- }
757
- throw error
758
- }
759
- }
760
- }
761
-
762
- const _getTokenBalance = _wrapAndHandleAccountNotFound(this.getTokenBalance, '0')
763
- const _getDecimals = _wrapAndHandleAccountNotFound(this.getDecimals, 0)
764
- const _getSupply = _wrapAndHandleAccountNotFound(this.getSupply, '0')
765
-
766
- const resolveTokens = tokenAccounts.map(async (account) => {
767
- try {
768
- const [_tokenMetaPlex, currentAmount, decimal] = await Promise.all([
769
- this.getMetaplexMetadata(account.mint),
770
- _getTokenBalance(account.address),
771
- _getDecimals(account.mint),
772
- ])
773
-
774
- const tokenMetaPlex = _tokenMetaPlex || { name: null, symbol: null }
775
- let nft = {
776
- collectionId: null,
777
- collectionName: null,
778
- collectionTitle: null,
779
- }
780
-
781
- // Only perform an NFT check (getSupply) if decimal is zero
782
- if (decimal === 0 && (await _getSupply(account.mint)) === '1') {
783
- try {
784
- const {
785
- id: collectionId,
786
- collectionName,
787
- collectionTitle,
788
- } = await magicEden.api.getNFTByMintAddress(account.mint)
789
- nft = {
790
- collectionId,
791
- collectionTitle,
792
- collectionName,
793
- }
794
- tokenMetaPlex.name = tokenMetaPlex.name || collectionTitle
795
- tokenMetaPlex.symbol = tokenMetaPlex.symbol || collectionName
796
- } catch (error) {
797
- console.warn(error)
798
- }
799
- }
800
-
801
- const balance = computeBalance(account.amount, currentAmount)
802
- return {
803
- balance,
804
- decimal,
805
- nft,
806
- address: account.address,
807
- mint: account.mint,
808
- name: tokenMetaPlex.name,
809
- symbol: tokenMetaPlex.symbol,
810
- type: 'TOKEN',
811
- }
812
- } catch (error) {
813
- console.warn(error)
814
- return {
815
- balance: null,
816
- }
817
- }
818
- })
819
-
820
- const accounts = await Promise.all([...resolveSols, ...resolveTokens])
821
- accounts.forEach((account) => {
822
- if (account.balance === null) {
823
- return
824
- }
825
-
826
- if (account.balance > 0) {
827
- willReceive.push(account)
828
- } else {
829
- willSend.push(account)
830
- }
831
- })
832
-
833
- return {
834
- willReceive,
835
- willSend,
836
- }
837
- }
838
-
839
- /**
840
- * Simulate transaction and return side effects
841
- */
842
- simulateAndRetrieveSideEffects = async (
843
- transactionMessage: SolanaWeb3Message,
844
- publicKey: string
845
- ) => {
846
- const { config, accountAddresses } = getTransactionSimulationParams(transactionMessage)
847
- const signatures = new Array(transactionMessage.header.numRequiredSignatures || 1).fill(null)
848
- const encodedTransaction = buildRawTransaction(
849
- transactionMessage.serialize(),
850
- signatures
851
- ).toString('base64')
852
- const futureAccountsState = await this.simulateTransaction(encodedTransaction, config)
853
- const { solAccounts, tokenAccounts } = filterAccountsByOwner(
854
- futureAccountsState,
855
- accountAddresses,
856
- publicKey
857
- )
858
-
859
- return this.resolveSimulationSideEffects(solAccounts, tokenAccounts)
860
- }
861
- }
862
-
863
- export default new Api()
1
+ import { Api } from './api'
2
+ export * from './api'
3
+ export * from './tx-log'
4
+ export * from './account-state'
5
+
6
+ // At some point we would like to exclude this export. Default export should be the whole asset "plugin" ready to be injected.
7
+ // Clients should not call an specific server api directly.
8
+ const serverApi = new Api()
9
+ export default serverApi