@exodus/solana-api 2.5.31-alpha.3 → 2.5.31

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "2.5.31-alpha.3",
3
+ "version": "2.5.31",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,11 +14,17 @@
14
14
  "publishConfig": {
15
15
  "access": "restricted"
16
16
  },
17
+ "scripts": {
18
+ "test": "run -T jest",
19
+ "lint": "run -T eslint ./src",
20
+ "lint:fix": "yarn lint --fix"
21
+ },
17
22
  "dependencies": {
18
23
  "@exodus/asset-json-rpc": "^1.0.0",
19
24
  "@exodus/asset-lib": "^4.0.0",
20
25
  "@exodus/assets": "^9.0.1",
21
26
  "@exodus/basic-utils": "^2.1.0",
27
+ "@exodus/currency": "^2.3.2",
22
28
  "@exodus/fetch": "^1.2.0",
23
29
  "@exodus/models": "^10.1.0",
24
30
  "@exodus/nfts-core": "^0.5.0",
@@ -27,12 +33,16 @@
27
33
  "@exodus/solana-meta": "^1.0.7",
28
34
  "bn.js": "^4.11.0",
29
35
  "debug": "^4.1.1",
36
+ "delay": "^4.0.1",
30
37
  "lodash": "^4.17.11",
38
+ "make-concurrent": "^4.0.0",
39
+ "minimalistic-assert": "^1.0.1",
40
+ "ms": "^2.1.3",
31
41
  "url-join": "4.0.0",
32
42
  "wretch": "^1.5.2"
33
43
  },
34
44
  "devDependencies": {
35
- "@exodus/assets-testing": "file:../../../__testing__"
45
+ "@exodus/assets-testing": "^1.0.0"
36
46
  },
37
47
  "gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
38
48
  }
@@ -27,6 +27,7 @@ export class SolanaAccountState extends AccountState {
27
27
  accounts: {}, // stake accounts
28
28
  },
29
29
  }
30
+
30
31
  static _tokens = [asset, ...tokens] // deprecated - will be removed
31
32
 
32
33
  static _postParse(data) {
package/src/api.js CHANGED
@@ -25,7 +25,6 @@ import { Connection } from './connection'
25
25
  const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com
26
26
  const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // not standard across all node providers (we're compatible only with Quicknode)
27
27
  const FORCE_HTTP = true // use https over ws
28
- const TXS_LIMIT = 100
29
28
 
30
29
  // Tokens + SOL api support
31
30
  export class Api {
@@ -60,16 +59,18 @@ export class Api {
60
59
  async watchAddress({
61
60
  address,
62
61
  tokensAddresses = [],
62
+ onMessage,
63
63
  handleAccounts,
64
64
  handleTransfers,
65
65
  handleReconnect,
66
66
  reconnectDelay,
67
67
  }) {
68
- if (FORCE_HTTP) return false
68
+ if (this.connections[address]) return // already subscribed
69
69
  const conn = new Connection({
70
70
  endpoint: this.wsUrl,
71
71
  address,
72
72
  tokensAddresses,
73
+ onMsg: (json) => onMessage(json),
73
74
  callback: (updates) =>
74
75
  this.handleUpdates({ updates, address, handleAccounts, handleTransfers }),
75
76
  reconnectCallback: handleReconnect,
@@ -98,6 +99,7 @@ export class Api {
98
99
  if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
99
100
  return connection.sendMessage(method, params)
100
101
  }
102
+
101
103
  // http fallback
102
104
  return this.api.post({ method, params })
103
105
  }
@@ -120,7 +122,7 @@ export class Api {
120
122
  return state
121
123
  }
122
124
 
123
- async getRecentBlockHash(commitment?) {
125
+ async getRecentBlockHash(commitment) {
124
126
  const result = await this.rpcCall(
125
127
  'getLatestBlockhash',
126
128
  [{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
@@ -169,7 +171,7 @@ export class Api {
169
171
  // cursor is a txHash
170
172
 
171
173
  try {
172
- let until = cursor
174
+ const until = cursor
173
175
 
174
176
  const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
175
177
  const tokenAccountAddresses = tokenAccountsByOwner
@@ -186,7 +188,7 @@ export class Api {
186
188
  })
187
189
  )
188
190
  )
189
- let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []).slice(0, TXS_LIMIT) // merge arrays
191
+ let txsId = txsResultsByAccount.flat() // merge arrays
190
192
  txsId = lodash.uniqBy(txsId, 'signature')
191
193
 
192
194
  // get txs details in parallel
@@ -236,14 +238,8 @@ export class Api {
236
238
  tokenAccountsByOwner,
237
239
  { includeUnparsed = false } = {}
238
240
  ) {
239
- let {
240
- fee,
241
- preBalances,
242
- postBalances,
243
- preTokenBalances,
244
- postTokenBalances,
245
- innerInstructions,
246
- } = txDetails.meta
241
+ let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
242
+ txDetails.meta
247
243
  preBalances = preBalances || []
248
244
  postBalances = postBalances || []
249
245
  preTokenBalances = preTokenBalances || []
@@ -339,7 +335,7 @@ export class Api {
339
335
  }))
340
336
  innerInstructions = innerInstructions
341
337
  .reduce((acc, val) => {
342
- return acc.concat(val.instructions)
338
+ return [...acc, ...val.instructions]
343
339
  }, [])
344
340
  .map((ix) => {
345
341
  const type = lodash.get(ix, 'parsed.type')
@@ -355,7 +351,7 @@ export class Api {
355
351
  const tokenAccount = tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
356
352
  return [source, destination].includes(tokenAccountAddress)
357
353
  })
358
- const isSending = !!tokenAccountsByOwner.find(({ tokenAccountAddress }) => {
354
+ const isSending = tokenAccountsByOwner.some(({ tokenAccountAddress }) => {
359
355
  return [source].includes(tokenAccountAddress)
360
356
  })
361
357
 
@@ -382,7 +378,7 @@ export class Api {
382
378
  const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
383
379
  const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
384
380
  const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
385
- const hasSolanaTx = solanaTx && !preTokenBalances.length && !postTokenBalances.length // only SOL moved and no tokens movements
381
+ const hasSolanaTx = solanaTx && preTokenBalances.length === 0 && postTokenBalances.length === 0 // only SOL moved and no tokens movements
386
382
 
387
383
  let tx = {}
388
384
  if (hasSolanaTx) {
@@ -449,7 +445,7 @@ export class Api {
449
445
  Array.isArray(tokenAccountsByOwner),
450
446
  'tokenAccountsByOwner is required when parsing token tx'
451
447
  )
452
- let tokenTxs = lodash
448
+ const tokenTxs = lodash
453
449
  .filter(instructions, ({ program, type }) => {
454
450
  return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
455
451
  }) // get Token transfer: could have more than 1 instructions
@@ -476,7 +472,7 @@ export class Api {
476
472
  }
477
473
  })
478
474
 
479
- if (tokenTxs.length) {
475
+ if (tokenTxs.length > 0) {
480
476
  // found spl-token simple transfer/transferChecked instruction
481
477
  // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
482
478
  tx = tokenTxs.reduce((finalTx, ix) => {
@@ -491,9 +487,11 @@ export class Api {
491
487
  const accountIndexes = lodash.mapKeys(accountKeys, (x, i) => i)
492
488
  Object.values(accountIndexes).forEach((acc) => {
493
489
  // filter by ownerAddress
490
+ // eslint-disable-next-line unicorn/prefer-array-some
494
491
  const hasKnownOwner = !!lodash.find(tokenAccountsByOwner, {
495
492
  tokenAccountAddress: acc.pubkey,
496
493
  })
494
+ // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
497
495
  acc.owner = hasKnownOwner ? ownerAddress : null
498
496
  })
499
497
 
@@ -509,15 +507,15 @@ export class Api {
509
507
  )
510
508
  })
511
509
 
512
- if (preBalances.length || postBalances.length) {
510
+ if (preBalances.length > 0 || postBalances.length > 0) {
513
511
  tx = {}
514
512
 
515
- if (includeUnparsed && innerInstructions.length) {
513
+ if (includeUnparsed && innerInstructions.length > 0) {
516
514
  // when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
517
515
  // 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
518
516
  // 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
519
517
  // SOL->SPL swaps on Raydium and Orca.
520
- tx = getUnparsedTx(tx)
518
+ tx = getUnparsedTx()
521
519
  tx.dexTxs = getInnerTxsFromBalanceChanges()
522
520
  } else {
523
521
  if (solanaTx) {
@@ -532,7 +530,7 @@ export class Api {
532
530
  }
533
531
 
534
532
  // If it has inner instructions then it's a DEX tx that moved SPL -> SPL
535
- if (innerInstructions.length) {
533
+ if (innerInstructions.length > 0) {
536
534
  tx.dexTxs = innerInstructions
537
535
  // if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
538
536
  if (!tx.from && !solanaTx) {
@@ -548,7 +546,7 @@ export class Api {
548
546
  const unparsed = Object.keys(tx).length === 0
549
547
 
550
548
  if (unparsed && includeUnparsed) {
551
- tx = getUnparsedTx(tx)
549
+ tx = getUnparsedTx()
552
550
  }
553
551
 
554
552
  // How tokens tx are parsed:
@@ -572,7 +570,7 @@ export class Api {
572
570
 
573
571
  async getWalletTokensList({ tokenAccounts }) {
574
572
  const tokensMint = []
575
- for (let account of tokenAccounts) {
573
+ for (const account of tokenAccounts) {
576
574
  const mint = account.mintAddress
577
575
 
578
576
  // skip cached NFT
@@ -585,6 +583,7 @@ export class Api {
585
583
  this.tokensToSkip[mint] = true
586
584
  continue
587
585
  }
586
+
588
587
  // OK
589
588
  tokensMint.push(mint)
590
589
  }
@@ -600,7 +599,7 @@ export class Api {
600
599
  )
601
600
 
602
601
  const tokenAccounts = []
603
- for (let entry of accountsList) {
602
+ for (const entry of accountsList) {
604
603
  const { pubkey, account } = entry
605
604
 
606
605
  const mint = lodash.get(account, 'data.parsed.info.mint')
@@ -618,6 +617,7 @@ export class Api {
618
617
  mintAddress: mint,
619
618
  })
620
619
  }
620
+
621
621
  // eventually filter by token
622
622
  return tokenTicker
623
623
  ? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
@@ -625,18 +625,24 @@ export class Api {
625
625
  }
626
626
 
627
627
  async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
628
- let accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
628
+ const accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
629
629
 
630
- const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
631
- if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
630
+ return accounts.reduce((acc, { tokenName, balance }) => {
631
+ if (
632
+ tokenName === 'unknown' ||
633
+ (filterByTokens.length > 0 && !filterByTokens.includes(tokenName))
634
+ )
632
635
  return acc // filter by supported tokens only
633
- if (!acc[tokenName]) acc[tokenName] = Number(balance)
636
+ if (acc[tokenName]) {
637
+ acc[tokenName] += Number(balance)
638
+ }
634
639
  // e.g { 'serum': 123 }
635
- else acc[tokenName] += Number(balance) // merge same token account balance
640
+ else {
641
+ acc[tokenName] = Number(balance)
642
+ } // merge same token account balance
643
+
636
644
  return acc
637
645
  }, {})
638
-
639
- return tokensBalance
640
646
  }
641
647
 
642
648
  async isAssociatedTokenAccountActive(tokenAddress) {
@@ -644,7 +650,7 @@ export class Api {
644
650
  try {
645
651
  await this.rpcCall('getTokenAccountBalance', [tokenAddress])
646
652
  return true
647
- } catch (e) {
653
+ } catch {
648
654
  return false
649
655
  }
650
656
  }
@@ -697,20 +703,19 @@ export class Api {
697
703
  return account.owner === SYSTEM_PROGRAM_ID.toBase58()
698
704
  ? 'solana'
699
705
  : account.owner === TOKEN_PROGRAM_ID.toBase58()
700
- ? 'token'
701
- : null
706
+ ? 'token'
707
+ : null
702
708
  }
703
709
 
704
710
  async getTokenAddressOwner(address) {
705
711
  const value = await this.getAccountInfo(address)
706
- const owner = lodash.get(value, 'data.parsed.info.owner', null)
707
- return owner
712
+ return lodash.get(value, 'data.parsed.info.owner', null)
708
713
  }
709
714
 
710
715
  async getAddressMint(address) {
711
716
  const value = await this.getAccountInfo(address)
712
- const mintAddress = lodash.get(value, 'data.parsed.info.mint', null) // token mint
713
- return mintAddress
717
+ // token mint
718
+ return lodash.get(value, 'data.parsed.info.mint', null)
714
719
  }
715
720
 
716
721
  async isTokenAddress(address) {
@@ -745,7 +750,7 @@ export class Api {
745
750
  let locked = 0
746
751
  let withdrawable = 0
747
752
  let pending = 0
748
- for (let entry of res) {
753
+ for (const entry of res) {
749
754
  const addr = entry.pubkey
750
755
  const lamports = lodash.get(entry, 'account.lamports', 0)
751
756
  const delegation = lodash.get(entry, 'account.data.parsed.info.stake.delegation', {})
@@ -766,11 +771,12 @@ export class Api {
766
771
  withdrawable += accounts[addr].canWithdraw ? lamports : 0
767
772
  pending += accounts[addr].isDeactivating ? lamports : 0
768
773
  }
774
+
769
775
  return { accounts, totalStake, locked, withdrawable, pending }
770
776
  }
771
777
 
772
778
  async getRewards(stakingAddresses = []) {
773
- if (!stakingAddresses.length) return 0
779
+ if (stakingAddresses.length === 0) return 0
774
780
 
775
781
  // custom endpoint!
776
782
  const rewards = await this.request('rewards')
@@ -780,11 +786,9 @@ export class Api {
780
786
  .json()
781
787
 
782
788
  // sum rewards for all addresses
783
- const earnings = Object.values(rewards).reduce((total, x) => {
789
+ return Object.values(rewards).reduce((total, x) => {
784
790
  return total + x
785
791
  }, 0)
786
-
787
- return earnings
788
792
  }
789
793
 
790
794
  async getMinimumBalanceForRentExemption(size) {
@@ -820,7 +824,7 @@ export class Api {
820
824
  } catch (error) {
821
825
  if (
822
826
  error.message &&
823
- !errorMessagesToRetry.find((errorMessage) => error.message.includes(errorMessage))
827
+ !errorMessagesToRetry.some((errorMessage) => error.message.includes(errorMessage))
824
828
  ) {
825
829
  error.finalError = true
826
830
  }
@@ -868,6 +872,7 @@ export class Api {
868
872
  if (error.message && error.message.includes('could not find account')) {
869
873
  return defaultValue
870
874
  }
875
+
871
876
  throw error
872
877
  }
873
878
  }
@@ -959,11 +964,12 @@ export class Api {
959
964
  simulateAndRetrieveSideEffects = async (
960
965
  message,
961
966
  publicKey,
962
- transactionMessage? // decompiled TransactionMessage
967
+ transactionMessage // decompiled TransactionMessage
963
968
  ) => {
964
969
  const { config, accountAddresses } = getTransactionSimulationParams(
965
970
  transactionMessage || message
966
971
  )
972
+ // eslint-disable-next-line unicorn/no-new-array
967
973
  const signatures = new Array(message.header.numRequiredSignatures || 1).fill(null)
968
974
  const encodedTransaction = buildRawTransaction(
969
975
  Buffer.from(message.serialize()),
package/src/connection.js CHANGED
@@ -20,6 +20,7 @@ export class Connection {
20
20
  address,
21
21
  tokensAddresses = [],
22
22
  callback,
23
+ onMsg,
23
24
  reconnectCallback = () => {},
24
25
  reconnectDelay = DEFAULT_RECONNECT_DELAY,
25
26
  }) {
@@ -27,6 +28,7 @@ export class Connection {
27
28
  this.tokensAddresses = tokensAddresses
28
29
  this.endpoint = endpoint
29
30
  this.callback = callback
31
+ this.onMsg = onMsg
30
32
  this.reconnectCallback = reconnectCallback
31
33
  this.reconnectDelay = reconnectDelay
32
34
 
@@ -66,10 +68,10 @@ export class Connection {
66
68
  reqUrl = `${obj}`
67
69
  debug('Opening WS to:', reqUrl)
68
70
  const ws = new WebSocket(`${reqUrl}`)
69
- ws.onmessage = this.onMessage.bind(this)
70
- ws.onopen = this.onOpen.bind(this)
71
- ws.onclose = this.onClose.bind(this)
72
- ws.onerror = this.onError.bind(this)
71
+ ws.addEventListener('message', this.onMessage.bind(this))
72
+ ws.addEventListener('open', this.onOpen.bind(this))
73
+ ws.addEventListener('close', this.onClose.bind(this))
74
+ ws.addEventListener('error', this.onError.bind(this))
73
75
  return ws
74
76
  }
75
77
 
@@ -90,14 +92,14 @@ export class Connection {
90
92
  }
91
93
 
92
94
  get running() {
93
- return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length)
95
+ return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length > 0)
94
96
  }
95
97
 
96
98
  get connectionState() {
97
99
  if (this.isConnecting) return 'CONNECTING'
98
- else if (this.isOpen) return 'OPEN'
99
- else if (this.isClosing) return 'CLOSING'
100
- else if (this.isClosed) return 'CLOSED'
100
+ if (this.isOpen) return 'OPEN'
101
+ if (this.isClosing) return 'CLOSING'
102
+ if (this.isClosed) return 'CLOSED'
101
103
  return 'NONE'
102
104
  }
103
105
 
@@ -125,7 +127,13 @@ export class Connection {
125
127
  try {
126
128
  const json = JSON.parse(evt.data)
127
129
  debug('new ws msg:', json)
128
- if (!json.error) {
130
+ if (json.error) {
131
+ if (lodash.get(this.rpcQueue, json.id)) {
132
+ this.rpcQueue[json.id].reject(new Error(json.error.message))
133
+ clearTimeout(this.rpcQueue[json.id].timeout)
134
+ delete this.rpcQueue[json.id]
135
+ } else debug('Unsupported WS message:', json.error.message)
136
+ } else {
129
137
  if (lodash.get(this.rpcQueue, json.id)) {
130
138
  // json-rpc reply
131
139
  clearTimeout(this.rpcQueue[json.id].timeout)
@@ -136,13 +144,8 @@ export class Connection {
136
144
  debug('pushing msg to queue', msg)
137
145
  this.messageQueue.push(msg) // sub results
138
146
  }
139
- this.processMessages()
140
- } else {
141
- if (lodash.get(this.rpcQueue, json.id)) {
142
- this.rpcQueue[json.id].reject(new Error(json.error.message))
143
- clearTimeout(this.rpcQueue[json.id].timeout)
144
- delete this.rpcQueue[json.id]
145
- } else debug('Unsupported WS message:', json.error.message)
147
+
148
+ this.processMessages(json)
146
149
  }
147
150
  } catch (e) {
148
151
  debug(e)
@@ -153,8 +156,8 @@ export class Connection {
153
156
  onOpen(evt) {
154
157
  debug('Opened WS')
155
158
  // subscribe to each addresses (SOL and ASA addr)
156
-
157
- this.tokensAddresses.concat(this.address).forEach((address) => {
159
+ const addresses = [...this.tokensAddresses, this.address]
160
+ addresses.forEach((address) => {
158
161
  // sub for account state changes
159
162
  this.ws.send(
160
163
  JSON.stringify({
@@ -195,11 +198,12 @@ export class Connection {
195
198
  }
196
199
  }
197
200
 
198
- async processMessages() {
201
+ async processMessages(json) {
202
+ if (this.onMsg) await this.onMsg(json)
199
203
  if (this.inProcessMessages) return null
200
204
  this.inProcessMessages = true
201
205
  try {
202
- while (this.messageQueue.length) {
206
+ while (this.messageQueue.length > 0) {
203
207
  const items = this.messageQueue.splice(0, this.messageQueue.length)
204
208
  await this.callback(items)
205
209
  }
@@ -3,35 +3,33 @@ import { TxSet } from '@exodus/models'
3
3
  // staking may be a feature that may not be available for a given wallet.
4
4
  // In this case, The wallet should exclude the staking balance from the general balance
5
5
 
6
- export const getBalancesFactory = ({ stakingFeatureAvailable }) => ({
7
- asset,
8
- accountState,
9
- txLog,
10
- }) => {
11
- const zero = asset.currency.ZERO
12
- const { balance, locked, withdrawable, pending } = fixBalances({
13
- txLog,
14
- balance: getBalanceFromAccountState({ asset, accountState }),
15
- locked: accountState.mem?.locked || zero,
16
- withdrawable: accountState.mem?.withdrawable || zero,
17
- pending: accountState.mem?.pending || zero,
18
- asset,
19
- })
20
- if (asset.baseAsset.name !== asset.name) {
21
- return { balance, spendableBalance: balance }
22
- }
6
+ export const getBalancesFactory =
7
+ ({ stakingFeatureAvailable }) =>
8
+ ({ asset, accountState, txLog }) => {
9
+ const zero = asset.currency.ZERO
10
+ const { balance, locked, withdrawable, pending } = fixBalances({
11
+ txLog,
12
+ balance: getBalanceFromAccountState({ asset, accountState }),
13
+ locked: accountState.mem?.locked || zero,
14
+ withdrawable: accountState.mem?.withdrawable || zero,
15
+ pending: accountState.mem?.pending || zero,
16
+ asset,
17
+ })
18
+ if (asset.baseAsset.name !== asset.name) {
19
+ return { balance, spendableBalance: balance }
20
+ }
23
21
 
24
- const balanceWithoutStaking = balance
25
- .sub(locked)
26
- .sub(withdrawable)
27
- .sub(pending)
28
- .clampLowerZero()
22
+ const balanceWithoutStaking = balance
23
+ .sub(locked)
24
+ .sub(withdrawable)
25
+ .sub(pending)
26
+ .clampLowerZero()
29
27
 
30
- return {
31
- balance: stakingFeatureAvailable ? balance : balanceWithoutStaking,
32
- spendableBalance: balanceWithoutStaking.sub(asset.accountReserve || zero).clampLowerZero(),
28
+ return {
29
+ balance: stakingFeatureAvailable ? balance : balanceWithoutStaking,
30
+ spendableBalance: balanceWithoutStaking.sub(asset.accountReserve || zero).clampLowerZero(),
31
+ }
33
32
  }
34
- }
35
33
 
36
34
  const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pending, asset }) => {
37
35
  for (const tx of txLog) {
@@ -40,10 +38,7 @@ const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pendi
40
38
  balance = balance.sub(tx.feeAmount)
41
39
  }
42
40
 
43
- if (!tx.data.staking) {
44
- // coinAmount is negative for sent tx
45
- balance = balance.sub(tx.coinAmount.abs())
46
- } else {
41
+ if (tx.data.staking) {
47
42
  // staking tx
48
43
  switch (tx.data.staking?.method) {
49
44
  case 'delegate':
@@ -57,9 +52,13 @@ const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pendi
57
52
  locked = asset.currency.ZERO
58
53
  break
59
54
  }
55
+ } else {
56
+ // coinAmount is negative for sent tx
57
+ balance = balance.sub(tx.coinAmount.abs())
60
58
  }
61
59
  }
62
60
  }
61
+
63
62
  return {
64
63
  balance: balance.clampLowerZero(),
65
64
  locked: locked.clampLowerZero(),
package/src/index.js CHANGED
@@ -4,7 +4,6 @@ import assetsList from '@exodus/solana-meta'
4
4
 
5
5
  import { Api } from './api'
6
6
 
7
- export { Api }
8
7
  export { default as SolanaFeeMonitor } from './fee-monitor'
9
8
  export { SolanaMonitor } from './tx-log'
10
9
  export { SolanaAccountState } from './account-state'
@@ -26,3 +25,5 @@ const assets = connectAssets(keyBy(assetsList, (asset) => asset.name))
26
25
  // Clients should not call an specific server api directly.
27
26
  const serverApi = new Api({ assets })
28
27
  export default serverApi
28
+
29
+ export { Api } from './api'
@@ -9,9 +9,7 @@ export const getSolStakedFee = ({ asset, stakingInfo, fee }) => {
9
9
  const { accounts } = stakingInfo
10
10
 
11
11
  const allPending = Object.entries(accounts).length
12
- const pendingFee = allPending > 0 ? fee.mul(allPending) : currency.ZERO
13
-
14
- return pendingFee
12
+ return allPending > 0 ? fee.mul(allPending) : currency.ZERO
15
13
  }
16
14
 
17
15
  export const getStakingInfo = (accountMem) => {
@@ -10,25 +10,14 @@ const DEFAULT_REMOTE_CONFIG = {
10
10
  staking: { enabled: true, pool: DEFAULT_POOL_ADDRESS },
11
11
  }
12
12
 
13
- const TICKS_BETWEEN_HISTORY_FETCHES = 10
14
- const TICKS_BETWEEN_STAKE_FETCHES = 5
15
-
16
13
  export class SolanaMonitor extends BaseMonitor {
17
- constructor({
18
- api,
19
- includeUnparsed = false,
20
- ticksBetweenHistoryFetches = TICKS_BETWEEN_HISTORY_FETCHES,
21
- ticksBetweenStakeFetches = TICKS_BETWEEN_STAKE_FETCHES,
22
- ...args
23
- }) {
14
+ constructor({ api, includeUnparsed = false, ...args }) {
24
15
  super(args)
25
16
  assert(api, 'api is required')
26
17
  this.api = api
27
18
  this.cursors = {}
28
19
  this.assets = {}
29
20
  this.staking = DEFAULT_REMOTE_CONFIG.staking
30
- this.ticksBetweenStakeFetches = ticksBetweenStakeFetches
31
- this.ticksBetweenHistoryFetches = ticksBetweenHistoryFetches
32
21
  this.includeUnparsed = includeUnparsed
33
22
  this.addHook('before-stop', (...args) => this.beforeStop(...args))
34
23
  }
@@ -52,15 +41,10 @@ export class SolanaMonitor extends BaseMonitor {
52
41
  })
53
42
  return this.api.watchAddress({
54
43
  address,
55
- /*
56
- // OPTIONAL. Relying on polling through ws
57
- tokensAddresses: [], // needed for ASA subs
58
- handleAccounts: (updates) => this.accountsCallback({ updates, walletAccount }),
59
- handleTransfers: (txs) => {
60
- // new SOL tx, ticking monitor
61
- this.tick({ walletAccount }) // it will cause refresh for both sender/receiver. Without necessarily fetching the tx if it's not finalized in the node.
44
+ onMessage: (json) => {
45
+ // new SOL tx event, tick monitor with 15 sec delay (to avoid hitting delayed nodes)
46
+ setTimeout(() => this.tick({ walletAccount }), 15_000)
62
47
  },
63
- */
64
48
  })
65
49
  }
66
50
 
@@ -97,24 +81,10 @@ export class SolanaMonitor extends BaseMonitor {
97
81
 
98
82
  async getStakingAddressesFromTxLog({ assetName, walletAccount }) {
99
83
  const txLog = await this.aci.getTxLog({ assetName: this.asset.name, walletAccount })
100
- const stakingAddresses = Array.from(txLog)
84
+ const stakingAddresses = [...txLog]
101
85
  .filter((tx) => _.get(tx, 'data.staking.stakeAddresses'))
102
86
  .map((tx) => tx.data.staking.stakeAddresses)
103
- return _.uniq(_.flatten(stakingAddresses))
104
- }
105
-
106
- balanceChanged({ account, newAccount }) {
107
- const solBalanceChanged = !account.balance || !account.balance.equals(newAccount.balance)
108
- if (solBalanceChanged) return true
109
-
110
- const tokenBalanceChanged =
111
- !account.tokenBalances ||
112
- Object.entries(newAccount.tokenBalances).some(
113
- ([token, balance]) =>
114
- !account.tokenBalances[token] || !account.tokenBalances[token].equals(balance)
115
- )
116
-
117
- return tokenBalanceChanged
87
+ return _.uniq(stakingAddresses.flat())
118
88
  }
119
89
 
120
90
  async tick({ walletAccount, refresh }) {
@@ -129,43 +99,28 @@ export class SolanaMonitor extends BaseMonitor {
129
99
  const address = await this.aci.getReceiveAddress({ assetName, walletAccount, useCache: true })
130
100
  const stakingAddresses = await this.getStakingAddressesFromTxLog({ assetName, walletAccount })
131
101
 
132
- const fetchStakingInfo = this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
133
- const staking = fetchStakingInfo
134
- ? await this.getStakingInfo({ address, stakingAddresses })
135
- : accountState.mem
136
-
137
- const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
138
- const account = await this.getAccount({ address, staking, tokenAccounts })
102
+ const { logItemsByAsset, hasNewTxs, cursorState } = await this.getHistory({
103
+ address,
104
+ accountState,
105
+ walletAccount,
106
+ refresh,
107
+ })
139
108
 
140
- const balanceChanged = this.balanceChanged({ account: accountState, newAccount: account })
109
+ const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
141
110
 
142
- const isHistoryUpdateTick =
143
- this.tickCount[walletAccount] % this.ticksBetweenHistoryFetches === 0
111
+ if (refresh || hasNewTxs || cursorChanged) {
112
+ const staking =
113
+ refresh || cursorChanged
114
+ ? await this.getStakingInfo({ address, stakingAddresses })
115
+ : accountState.mem
144
116
 
145
- const shouldUpdateHistory = refresh || isHistoryUpdateTick || balanceChanged
146
- const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateHistory
147
- const shouldUpdateBalanceBeforeHistory = true
117
+ const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
118
+ const account = await this.getAccount({ address, staking, tokenAccounts })
148
119
 
149
- // getHistory is more likely to fail/be rate limited, so we want to update users balance only on a lot of ticks
150
- if (shouldUpdateBalanceBeforeHistory || shouldUpdateOnlyBalance) {
151
120
  // update all state at once
152
- await this.updateState({ account, walletAccount, staking })
153
121
  await this.emitUnknownTokensEvent({ tokenAccounts })
154
- }
155
- if (shouldUpdateHistory) {
156
- const { logItemsByAsset, cursorState } = await this.getHistory({
157
- address,
158
- accountState,
159
- walletAccount,
160
- refresh,
161
- })
162
-
163
- const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
164
-
165
- // update all state at once
166
122
  await this.updateTxLogByAsset({ walletAccount, logItemsByAsset, refresh })
167
123
  await this.updateState({ account, cursorState, walletAccount, staking })
168
- await this.emitUnknownTokensEvent({ tokenAccounts })
169
124
  if (refresh || cursorChanged) {
170
125
  this.cursors[walletAccount] = cursorState.cursor
171
126
  }
@@ -173,7 +128,7 @@ export class SolanaMonitor extends BaseMonitor {
173
128
  }
174
129
 
175
130
  async getHistory({ address, accountState, refresh } = {}) {
176
- let cursor = refresh ? '' : accountState.cursor
131
+ const cursor = refresh ? '' : accountState.cursor
177
132
  const baseAsset = this.asset
178
133
 
179
134
  const { transactions, newCursor } = await this.api.getTransactions(address, {
@@ -226,6 +181,7 @@ export class SolanaMonitor extends BaseMonitor {
226
181
 
227
182
  item.data.meta = tx.data.meta
228
183
  }
184
+
229
185
  if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
230
186
  const feeItem = {
231
187
  ..._.clone(item),
@@ -235,6 +191,7 @@ export class SolanaMonitor extends BaseMonitor {
235
191
  }
236
192
  mappedTransactions.push(feeItem)
237
193
  }
194
+
238
195
  mappedTransactions.push(item)
239
196
  }
240
197
 
package/src/tx-send.js CHANGED
@@ -1,183 +1,179 @@
1
1
  import { createUnsignedTx, findAssociatedTokenAddress } from '@exodus/solana-lib'
2
2
  import assert from 'minimalistic-assert'
3
3
 
4
- export const createAndBroadcastTXFactory = (api) => async (
5
- { asset, walletAccount, address, amount, options = {} },
6
- { assetClientInterface }
7
- ) => {
8
- const assetName = asset.name
9
- assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${assetName}`)
10
-
11
- const {
12
- feeAmount,
13
- method,
14
- stakeAddresses,
15
- seed,
16
- pool,
17
- customMintAddress,
18
- tokenStandard,
19
- // <MagicEden>
20
- initializerAddress,
21
- initializerDepositTokenAddress,
22
- takerAmount,
23
- escrowAddress,
24
- escrowBump,
25
- pdaAddress,
26
- takerAddress,
27
- expectedTakerAmount,
28
- expectedMintAddress,
29
- metadataAddress,
30
- creators,
31
- // </MagicEden>
32
- reference,
33
- memo,
34
- } = options
35
- const { baseAsset } = asset
36
- const from = await assetClientInterface.getReceiveAddress({
37
- assetName: baseAsset.name,
38
- walletAccount,
39
- })
40
-
41
- const isToken = asset.assetType === 'SOLANA_TOKEN'
42
-
43
- // Check if receiver has address active when sending tokens.
44
- if (isToken) {
45
- // check address mint is the same
46
- const targetMint = await api.getAddressMint(address) // null if it's a SOL address
47
- if (targetMint && targetMint !== asset.mintAddress) {
48
- const err = new Error('Wrong Destination Wallet')
49
- err.reason = { mintAddressMismatch: true }
50
- throw err
4
+ export const createAndBroadcastTXFactory =
5
+ (api) =>
6
+ async ({ asset, walletAccount, address, amount, options = {} }, { assetClientInterface }) => {
7
+ const assetName = asset.name
8
+ assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${assetName}`)
9
+
10
+ const {
11
+ feeAmount,
12
+ method,
13
+ stakeAddresses,
14
+ seed,
15
+ pool,
16
+ customMintAddress,
17
+ tokenStandard,
18
+ // <MagicEden>
19
+ initializerAddress,
20
+ initializerDepositTokenAddress,
21
+ takerAmount,
22
+ escrowAddress,
23
+ escrowBump,
24
+ pdaAddress,
25
+ takerAddress,
26
+ expectedTakerAmount,
27
+ expectedMintAddress,
28
+ metadataAddress,
29
+ creators,
30
+ // </MagicEden>
31
+ reference,
32
+ memo,
33
+ } = options
34
+ const { baseAsset } = asset
35
+ const from = await assetClientInterface.getReceiveAddress({
36
+ assetName: baseAsset.name,
37
+ walletAccount,
38
+ })
39
+
40
+ const isToken = asset.assetType === 'SOLANA_TOKEN'
41
+
42
+ // Check if receiver has address active when sending tokens.
43
+ if (isToken) {
44
+ // check address mint is the same
45
+ const targetMint = await api.getAddressMint(address) // null if it's a SOL address
46
+ if (targetMint && targetMint !== asset.mintAddress) {
47
+ const err = new Error('Wrong Destination Wallet')
48
+ err.reason = { mintAddressMismatch: true }
49
+ throw err
50
+ }
51
+ } else {
52
+ // sending SOL
53
+ const addressType = await api.getAddressType(address)
54
+ if (addressType === 'token') {
55
+ const err = new Error('Destination Wallet is a Token address')
56
+ err.reason = { wrongAddressType: true }
57
+ throw err
58
+ }
51
59
  }
52
- } else {
53
- // sending SOL
54
- const addressType = await api.getAddressType(address)
55
- if (addressType === 'token') {
56
- const err = new Error('Destination Wallet is a Token address')
57
- err.reason = { wrongAddressType: true }
58
- throw err
60
+
61
+ const recentBlockhash = options.recentBlockhash || (await api.getRecentBlockHash())
62
+
63
+ let tokenParams = Object.create(null)
64
+ if (isToken || customMintAddress) {
65
+ const tokenMintAddress = customMintAddress || asset.mintAddress
66
+ const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress)
67
+ const [destinationAddressType, isAssociatedTokenAccountActive, fromTokenAccountAddresses] =
68
+ await Promise.all([
69
+ api.getAddressType(address),
70
+ api.isAssociatedTokenAccountActive(tokenAddress),
71
+ api.getTokenAccountsByOwner(from),
72
+ ])
73
+
74
+ const fromTokenAddresses = fromTokenAccountAddresses.filter(
75
+ ({ mintAddress }) => mintAddress === tokenMintAddress
76
+ )
77
+
78
+ tokenParams = {
79
+ tokenMintAddress,
80
+ destinationAddressType,
81
+ isAssociatedTokenAccountActive,
82
+ fromTokenAddresses,
83
+ tokenStandard,
84
+ }
59
85
  }
60
- }
61
86
 
62
- const recentBlockhash = options.recentBlockhash || (await api.getRecentBlockHash())
63
-
64
- let tokenParams = Object.create(null)
65
- if (isToken || customMintAddress) {
66
- const tokenMintAddress = customMintAddress || asset.mintAddress
67
- const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress)
68
- const [
69
- destinationAddressType,
70
- isAssociatedTokenAccountActive,
71
- fromTokenAccountAddresses,
72
- ] = await Promise.all([
73
- api.getAddressType(address),
74
- api.isAssociatedTokenAccountActive(tokenAddress),
75
- api.getTokenAccountsByOwner(from),
76
- ])
77
-
78
- const fromTokenAddresses = fromTokenAccountAddresses.filter(
79
- ({ mintAddress }) => mintAddress === tokenMintAddress
80
- )
81
-
82
- tokenParams = {
83
- tokenMintAddress,
84
- destinationAddressType,
85
- isAssociatedTokenAccountActive,
86
- fromTokenAddresses,
87
- tokenStandard,
87
+ const stakingParams = {
88
+ method,
89
+ stakeAddresses,
90
+ seed,
91
+ pool,
88
92
  }
89
- }
90
93
 
91
- const stakingParams = {
92
- method,
93
- stakeAddresses,
94
- seed,
95
- pool,
96
- }
94
+ const magicEdenParams = {
95
+ method,
96
+ initializerAddress,
97
+ initializerDepositTokenAddress,
98
+ takerAmount,
99
+ escrowAddress,
100
+ escrowBump,
101
+ pdaAddress,
102
+ takerAddress,
103
+ expectedTakerAmount,
104
+ expectedMintAddress,
105
+ metadataAddress,
106
+ creators,
107
+ }
97
108
 
98
- const magicEdenParams = {
99
- method,
100
- initializerAddress,
101
- initializerDepositTokenAddress,
102
- takerAmount,
103
- escrowAddress,
104
- escrowBump,
105
- pdaAddress,
106
- takerAddress,
107
- expectedTakerAmount,
108
- expectedMintAddress,
109
- metadataAddress,
110
- creators,
111
- }
109
+ const unsignedTransaction = createUnsignedTx({
110
+ asset,
111
+ from,
112
+ to: address,
113
+ amount,
114
+ fee: feeAmount,
115
+ recentBlockhash,
116
+ reference,
117
+ memo,
118
+ ...tokenParams,
119
+ ...stakingParams,
120
+ ...magicEdenParams,
121
+ })
112
122
 
113
- const unsignedTransaction = createUnsignedTx({
114
- asset,
115
- from,
116
- to: address,
117
- amount,
118
- fee: feeAmount,
119
- recentBlockhash,
120
- reference,
121
- memo,
122
- ...tokenParams,
123
- ...stakingParams,
124
- ...magicEdenParams,
125
- })
126
-
127
- const { txId, rawTx } = await assetClientInterface.signTransaction({
128
- assetName: baseAsset.name,
129
- unsignedTx: unsignedTransaction,
130
- walletAccount,
131
- })
132
-
133
- await baseAsset.api.broadcastTx(rawTx)
134
-
135
- const selfSend = from === address
136
- const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
137
- const coinAmount = isStakingTx
138
- ? amount.abs()
139
- : selfSend
140
- ? asset.currency.ZERO
141
- : amount.abs().negate()
142
-
143
- const data = isStakingTx ? { staking: stakingParams } : Object.create(null)
144
- const tx = {
145
- txId,
146
- confirmations: 0,
147
- coinName: assetName,
148
- coinAmount,
149
- feeAmount,
150
- feeCoinName: asset.feeAsset.name,
151
- selfSend,
152
- to: address,
153
- data,
154
- currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
155
- }
156
- await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
123
+ const { txId, rawTx } = await assetClientInterface.signTransaction({
124
+ assetName: baseAsset.name,
125
+ unsignedTx: unsignedTransaction,
126
+ walletAccount,
127
+ })
128
+
129
+ await baseAsset.api.broadcastTx(rawTx)
157
130
 
158
- if (isToken) {
159
- // write tx entry in solana for token fee
160
- const txForFee = {
131
+ const selfSend = from === address
132
+ const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
133
+ const coinAmount = isStakingTx
134
+ ? amount.abs()
135
+ : selfSend
136
+ ? asset.currency.ZERO
137
+ : amount.abs().negate()
138
+
139
+ const data = isStakingTx ? { staking: stakingParams } : Object.create(null)
140
+ const tx = {
161
141
  txId,
162
142
  confirmations: 0,
163
- coinName: baseAsset.name,
164
- coinAmount: baseAsset.currency.ZERO,
165
- tokens: [assetName],
143
+ coinName: assetName,
144
+ coinAmount,
166
145
  feeAmount,
167
- feeCoinName: baseAsset.feeAsset.name,
168
- to: address,
146
+ feeCoinName: asset.feeAsset.name,
169
147
  selfSend,
170
- currencies: {
171
- [baseAsset.name]: baseAsset.currency,
172
- [baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
173
- },
148
+ to: address,
149
+ data,
150
+ currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
151
+ }
152
+ await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
153
+
154
+ if (isToken) {
155
+ // write tx entry in solana for token fee
156
+ const txForFee = {
157
+ txId,
158
+ confirmations: 0,
159
+ coinName: baseAsset.name,
160
+ coinAmount: baseAsset.currency.ZERO,
161
+ tokens: [assetName],
162
+ feeAmount,
163
+ feeCoinName: baseAsset.feeAsset.name,
164
+ to: address,
165
+ selfSend,
166
+ currencies: {
167
+ [baseAsset.name]: baseAsset.currency,
168
+ [baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
169
+ },
170
+ }
171
+ await assetClientInterface.updateTxLogAndNotify({
172
+ assetName: baseAsset.name,
173
+ walletAccount,
174
+ txs: [txForFee],
175
+ })
174
176
  }
175
- await assetClientInterface.updateTxLogAndNotify({
176
- assetName: baseAsset.name,
177
- walletAccount,
178
- txs: [txForFee],
179
- })
180
- }
181
177
 
182
- return { txId }
183
- }
178
+ return { txId }
179
+ }