@exodus/solana-api 2.5.31-alpha.1 → 2.5.31-alpha.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "2.5.31-alpha.1",
3
+ "version": "2.5.31-alpha.2",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -14,6 +14,11 @@
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",
@@ -32,7 +37,7 @@
32
37
  "wretch": "^1.5.2"
33
38
  },
34
39
  "devDependencies": {
35
- "@exodus/assets-testing": "file:../../../__testing__"
40
+ "@exodus/assets-testing": "^1.0.0"
36
41
  },
37
42
  "gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
38
43
  }
@@ -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,6 +25,7 @@ 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
28
29
 
29
30
  // Tokens + SOL api support
30
31
  export class Api {
@@ -59,18 +60,16 @@ export class Api {
59
60
  async watchAddress({
60
61
  address,
61
62
  tokensAddresses = [],
62
- onMessage,
63
63
  handleAccounts,
64
64
  handleTransfers,
65
65
  handleReconnect,
66
66
  reconnectDelay,
67
67
  }) {
68
- if (this.connections[address]) return // already subscribed
68
+ if (FORCE_HTTP) return false
69
69
  const conn = new Connection({
70
70
  endpoint: this.wsUrl,
71
71
  address,
72
72
  tokensAddresses,
73
- onMsg: (json) => onMessage(json),
74
73
  callback: (updates) =>
75
74
  this.handleUpdates({ updates, address, handleAccounts, handleTransfers }),
76
75
  reconnectCallback: handleReconnect,
@@ -99,6 +98,7 @@ export class Api {
99
98
  if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
100
99
  return connection.sendMessage(method, params)
101
100
  }
101
+
102
102
  // http fallback
103
103
  return this.api.post({ method, params })
104
104
  }
@@ -121,7 +121,7 @@ export class Api {
121
121
  return state
122
122
  }
123
123
 
124
- async getRecentBlockHash(commitment?) {
124
+ async getRecentBlockHash(commitment) {
125
125
  const result = await this.rpcCall(
126
126
  'getLatestBlockhash',
127
127
  [{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
@@ -170,7 +170,7 @@ export class Api {
170
170
  // cursor is a txHash
171
171
 
172
172
  try {
173
- let until = cursor
173
+ const until = cursor
174
174
 
175
175
  const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
176
176
  const tokenAccountAddresses = tokenAccountsByOwner
@@ -187,7 +187,7 @@ export class Api {
187
187
  })
188
188
  )
189
189
  )
190
- let txsId = txsResultsByAccount.reduce((arr, row) => arr.concat(row), []) // merge arrays
190
+ let txsId = txsResultsByAccount.flat().slice(0, TXS_LIMIT) // merge arrays
191
191
  txsId = lodash.uniqBy(txsId, 'signature')
192
192
 
193
193
  // get txs details in parallel
@@ -237,14 +237,8 @@ export class Api {
237
237
  tokenAccountsByOwner,
238
238
  { includeUnparsed = false } = {}
239
239
  ) {
240
- let {
241
- fee,
242
- preBalances,
243
- postBalances,
244
- preTokenBalances,
245
- postTokenBalances,
246
- innerInstructions,
247
- } = txDetails.meta
240
+ let { fee, preBalances, postBalances, preTokenBalances, postTokenBalances, innerInstructions } =
241
+ txDetails.meta
248
242
  preBalances = preBalances || []
249
243
  postBalances = postBalances || []
250
244
  preTokenBalances = preTokenBalances || []
@@ -383,7 +377,7 @@ export class Api {
383
377
  const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
384
378
  const stakeWithdraw = lodash.find(instructions, { program: 'stake', type: 'withdraw' })
385
379
  const stakeUndelegate = lodash.find(instructions, { program: 'stake', type: 'deactivate' })
386
- const hasSolanaTx = solanaTx && !preTokenBalances.length && !postTokenBalances.length // only SOL moved and no tokens movements
380
+ const hasSolanaTx = solanaTx && preTokenBalances.length === 0 && postTokenBalances.length === 0 // only SOL moved and no tokens movements
387
381
 
388
382
  let tx = {}
389
383
  if (hasSolanaTx) {
@@ -450,7 +444,7 @@ export class Api {
450
444
  Array.isArray(tokenAccountsByOwner),
451
445
  'tokenAccountsByOwner is required when parsing token tx'
452
446
  )
453
- let tokenTxs = lodash
447
+ const tokenTxs = lodash
454
448
  .filter(instructions, ({ program, type }) => {
455
449
  return program === 'spl-token' && ['transfer', 'transferChecked'].includes(type)
456
450
  }) // get Token transfer: could have more than 1 instructions
@@ -477,7 +471,7 @@ export class Api {
477
471
  }
478
472
  })
479
473
 
480
- if (tokenTxs.length) {
474
+ if (tokenTxs.length > 0) {
481
475
  // found spl-token simple transfer/transferChecked instruction
482
476
  // .reduce to sum/sub (based on isSending) all the same tokens amount (From instructions -> 1 single tx)
483
477
  tx = tokenTxs.reduce((finalTx, ix) => {
@@ -510,10 +504,10 @@ export class Api {
510
504
  )
511
505
  })
512
506
 
513
- if (preBalances.length || postBalances.length) {
507
+ if (preBalances.length > 0 || postBalances.length > 0) {
514
508
  tx = {}
515
509
 
516
- if (includeUnparsed && innerInstructions.length) {
510
+ if (includeUnparsed && innerInstructions.length > 0) {
517
511
  // when using includeUnparsed for DEX tx we want to keep SOL tx as "unparsed"
518
512
  // 1. we want to treat all SOL dex transactions as "Contract transaction", not "Sent SOL"
519
513
  // 2. default behavior is not perfect. For example it doesn't see SOL-side tx in
@@ -533,7 +527,7 @@ export class Api {
533
527
  }
534
528
 
535
529
  // If it has inner instructions then it's a DEX tx that moved SPL -> SPL
536
- if (innerInstructions.length) {
530
+ if (innerInstructions.length > 0) {
537
531
  tx.dexTxs = innerInstructions
538
532
  // if tx involves only SPL swaps. Expand DEX ix (first element as tx base and the other kept there)
539
533
  if (!tx.from && !solanaTx) {
@@ -573,7 +567,7 @@ export class Api {
573
567
 
574
568
  async getWalletTokensList({ tokenAccounts }) {
575
569
  const tokensMint = []
576
- for (let account of tokenAccounts) {
570
+ for (const account of tokenAccounts) {
577
571
  const mint = account.mintAddress
578
572
 
579
573
  // skip cached NFT
@@ -586,6 +580,7 @@ export class Api {
586
580
  this.tokensToSkip[mint] = true
587
581
  continue
588
582
  }
583
+
589
584
  // OK
590
585
  tokensMint.push(mint)
591
586
  }
@@ -601,7 +596,7 @@ export class Api {
601
596
  )
602
597
 
603
598
  const tokenAccounts = []
604
- for (let entry of accountsList) {
599
+ for (const entry of accountsList) {
605
600
  const { pubkey, account } = entry
606
601
 
607
602
  const mint = lodash.get(account, 'data.parsed.info.mint')
@@ -619,6 +614,7 @@ export class Api {
619
614
  mintAddress: mint,
620
615
  })
621
616
  }
617
+
622
618
  // eventually filter by token
623
619
  return tokenTicker
624
620
  ? tokenAccounts.filter(({ ticker }) => ticker === tokenTicker)
@@ -626,18 +622,24 @@ export class Api {
626
622
  }
627
623
 
628
624
  async getTokensBalance({ address, filterByTokens = [], tokenAccounts }) {
629
- let accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
625
+ const accounts = tokenAccounts || (await this.getTokenAccountsByOwner(address))
630
626
 
631
- const tokensBalance = accounts.reduce((acc, { tokenName, balance }) => {
632
- if (tokenName === 'unknown' || (filterByTokens.length && !filterByTokens.includes(tokenName)))
627
+ return accounts.reduce((acc, { tokenName, balance }) => {
628
+ if (
629
+ tokenName === 'unknown' ||
630
+ (filterByTokens.length > 0 && !filterByTokens.includes(tokenName))
631
+ )
633
632
  return acc // filter by supported tokens only
634
- if (!acc[tokenName]) acc[tokenName] = Number(balance)
633
+ if (acc[tokenName]) {
634
+ acc[tokenName] += Number(balance)
635
+ }
635
636
  // e.g { 'serum': 123 }
636
- else acc[tokenName] += Number(balance) // merge same token account balance
637
+ else {
638
+ acc[tokenName] = Number(balance)
639
+ } // merge same token account balance
640
+
637
641
  return acc
638
642
  }, {})
639
-
640
- return tokensBalance
641
643
  }
642
644
 
643
645
  async isAssociatedTokenAccountActive(tokenAddress) {
@@ -645,7 +647,7 @@ export class Api {
645
647
  try {
646
648
  await this.rpcCall('getTokenAccountBalance', [tokenAddress])
647
649
  return true
648
- } catch (e) {
650
+ } catch {
649
651
  return false
650
652
  }
651
653
  }
@@ -698,20 +700,19 @@ export class Api {
698
700
  return account.owner === SYSTEM_PROGRAM_ID.toBase58()
699
701
  ? 'solana'
700
702
  : account.owner === TOKEN_PROGRAM_ID.toBase58()
701
- ? 'token'
702
- : null
703
+ ? 'token'
704
+ : null
703
705
  }
704
706
 
705
707
  async getTokenAddressOwner(address) {
706
708
  const value = await this.getAccountInfo(address)
707
- const owner = lodash.get(value, 'data.parsed.info.owner', null)
708
- return owner
709
+ return lodash.get(value, 'data.parsed.info.owner', null)
709
710
  }
710
711
 
711
712
  async getAddressMint(address) {
712
713
  const value = await this.getAccountInfo(address)
713
- const mintAddress = lodash.get(value, 'data.parsed.info.mint', null) // token mint
714
- return mintAddress
714
+ // token mint
715
+ return lodash.get(value, 'data.parsed.info.mint', null)
715
716
  }
716
717
 
717
718
  async isTokenAddress(address) {
@@ -746,7 +747,7 @@ export class Api {
746
747
  let locked = 0
747
748
  let withdrawable = 0
748
749
  let pending = 0
749
- for (let entry of res) {
750
+ for (const entry of res) {
750
751
  const addr = entry.pubkey
751
752
  const lamports = lodash.get(entry, 'account.lamports', 0)
752
753
  const delegation = lodash.get(entry, 'account.data.parsed.info.stake.delegation', {})
@@ -767,11 +768,12 @@ export class Api {
767
768
  withdrawable += accounts[addr].canWithdraw ? lamports : 0
768
769
  pending += accounts[addr].isDeactivating ? lamports : 0
769
770
  }
771
+
770
772
  return { accounts, totalStake, locked, withdrawable, pending }
771
773
  }
772
774
 
773
775
  async getRewards(stakingAddresses = []) {
774
- if (!stakingAddresses.length) return 0
776
+ if (stakingAddresses.length === 0) return 0
775
777
 
776
778
  // custom endpoint!
777
779
  const rewards = await this.request('rewards')
@@ -781,11 +783,9 @@ export class Api {
781
783
  .json()
782
784
 
783
785
  // sum rewards for all addresses
784
- const earnings = Object.values(rewards).reduce((total, x) => {
786
+ return Object.values(rewards).reduce((total, x) => {
785
787
  return total + x
786
788
  }, 0)
787
-
788
- return earnings
789
789
  }
790
790
 
791
791
  async getMinimumBalanceForRentExemption(size) {
@@ -869,6 +869,7 @@ export class Api {
869
869
  if (error.message && error.message.includes('could not find account')) {
870
870
  return defaultValue
871
871
  }
872
+
872
873
  throw error
873
874
  }
874
875
  }
@@ -960,7 +961,7 @@ export class Api {
960
961
  simulateAndRetrieveSideEffects = async (
961
962
  message,
962
963
  publicKey,
963
- transactionMessage? // decompiled TransactionMessage
964
+ transactionMessage // decompiled TransactionMessage
964
965
  ) => {
965
966
  const { config, accountAddresses } = getTransactionSimulationParams(
966
967
  transactionMessage || message
package/src/connection.js CHANGED
@@ -20,7 +20,6 @@ export class Connection {
20
20
  address,
21
21
  tokensAddresses = [],
22
22
  callback,
23
- onMsg,
24
23
  reconnectCallback = () => {},
25
24
  reconnectDelay = DEFAULT_RECONNECT_DELAY,
26
25
  }) {
@@ -28,7 +27,6 @@ export class Connection {
28
27
  this.tokensAddresses = tokensAddresses
29
28
  this.endpoint = endpoint
30
29
  this.callback = callback
31
- this.onMsg = onMsg
32
30
  this.reconnectCallback = reconnectCallback
33
31
  this.reconnectDelay = reconnectDelay
34
32
 
@@ -69,8 +67,8 @@ export class Connection {
69
67
  debug('Opening WS to:', reqUrl)
70
68
  const ws = new WebSocket(`${reqUrl}`)
71
69
  ws.onmessage = this.onMessage.bind(this)
72
- ws.onopen = this.onOpen.bind(this)
73
- ws.onclose = this.onClose.bind(this)
70
+ ws.addEventListener('open', this.onOpen.bind(this))
71
+ ws.addEventListener('close', this.onClose.bind(this))
74
72
  ws.onerror = this.onError.bind(this)
75
73
  return ws
76
74
  }
@@ -92,14 +90,14 @@ export class Connection {
92
90
  }
93
91
 
94
92
  get running() {
95
- return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length)
93
+ return !!(!this.isClosed || this.inProcessMessages || this.messageQueue.length > 0)
96
94
  }
97
95
 
98
96
  get connectionState() {
99
97
  if (this.isConnecting) return 'CONNECTING'
100
- else if (this.isOpen) return 'OPEN'
101
- else if (this.isClosing) return 'CLOSING'
102
- else if (this.isClosed) return 'CLOSED'
98
+ if (this.isOpen) return 'OPEN'
99
+ if (this.isClosing) return 'CLOSING'
100
+ if (this.isClosed) return 'CLOSED'
103
101
  return 'NONE'
104
102
  }
105
103
 
@@ -127,7 +125,13 @@ export class Connection {
127
125
  try {
128
126
  const json = JSON.parse(evt.data)
129
127
  debug('new ws msg:', json)
130
- if (!json.error) {
128
+ if (json.error) {
129
+ if (lodash.get(this.rpcQueue, json.id)) {
130
+ this.rpcQueue[json.id].reject(new Error(json.error.message))
131
+ clearTimeout(this.rpcQueue[json.id].timeout)
132
+ delete this.rpcQueue[json.id]
133
+ } else debug('Unsupported WS message:', json.error.message)
134
+ } else {
131
135
  if (lodash.get(this.rpcQueue, json.id)) {
132
136
  // json-rpc reply
133
137
  clearTimeout(this.rpcQueue[json.id].timeout)
@@ -138,13 +142,8 @@ export class Connection {
138
142
  debug('pushing msg to queue', msg)
139
143
  this.messageQueue.push(msg) // sub results
140
144
  }
141
- this.processMessages(json)
142
- } else {
143
- if (lodash.get(this.rpcQueue, json.id)) {
144
- this.rpcQueue[json.id].reject(new Error(json.error.message))
145
- clearTimeout(this.rpcQueue[json.id].timeout)
146
- delete this.rpcQueue[json.id]
147
- } else debug('Unsupported WS message:', json.error.message)
145
+
146
+ this.processMessages()
148
147
  }
149
148
  } catch (e) {
150
149
  debug(e)
@@ -197,12 +196,11 @@ export class Connection {
197
196
  }
198
197
  }
199
198
 
200
- async processMessages(json) {
201
- if (this.onMsg) await this.onMsg(json)
199
+ async processMessages() {
202
200
  if (this.inProcessMessages) return null
203
201
  this.inProcessMessages = true
204
202
  try {
205
- while (this.messageQueue.length) {
203
+ while (this.messageQueue.length > 0) {
206
204
  const items = this.messageQueue.splice(0, this.messageQueue.length)
207
205
  await this.callback(items)
208
206
  }
@@ -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,14 +10,25 @@ 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
+
13
16
  export class SolanaMonitor extends BaseMonitor {
14
- constructor({ api, includeUnparsed = false, ...args }) {
17
+ constructor({
18
+ api,
19
+ includeUnparsed = false,
20
+ ticksBetweenHistoryFetches = TICKS_BETWEEN_HISTORY_FETCHES,
21
+ ticksBetweenStakeFetches = TICKS_BETWEEN_STAKE_FETCHES,
22
+ ...args
23
+ }) {
15
24
  super(args)
16
25
  assert(api, 'api is required')
17
26
  this.api = api
18
27
  this.cursors = {}
19
28
  this.assets = {}
20
29
  this.staking = DEFAULT_REMOTE_CONFIG.staking
30
+ this.ticksBetweenStakeFetches = ticksBetweenStakeFetches
31
+ this.ticksBetweenHistoryFetches = ticksBetweenHistoryFetches
21
32
  this.includeUnparsed = includeUnparsed
22
33
  this.addHook('before-stop', (...args) => this.beforeStop(...args))
23
34
  }
@@ -41,10 +52,15 @@ export class SolanaMonitor extends BaseMonitor {
41
52
  })
42
53
  return this.api.watchAddress({
43
54
  address,
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)
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.
47
62
  },
63
+ */
48
64
  })
49
65
  }
50
66
 
@@ -81,10 +97,24 @@ export class SolanaMonitor extends BaseMonitor {
81
97
 
82
98
  async getStakingAddressesFromTxLog({ assetName, walletAccount }) {
83
99
  const txLog = await this.aci.getTxLog({ assetName: this.asset.name, walletAccount })
84
- const stakingAddresses = Array.from(txLog)
100
+ const stakingAddresses = [...txLog]
85
101
  .filter((tx) => _.get(tx, 'data.staking.stakeAddresses'))
86
102
  .map((tx) => tx.data.staking.stakeAddresses)
87
- return _.uniq(_.flatten(stakingAddresses))
103
+ return _.uniq(stakingAddresses.flat())
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
88
118
  }
89
119
 
90
120
  async tick({ walletAccount, refresh }) {
@@ -99,28 +129,43 @@ export class SolanaMonitor extends BaseMonitor {
99
129
  const address = await this.aci.getReceiveAddress({ assetName, walletAccount, useCache: true })
100
130
  const stakingAddresses = await this.getStakingAddressesFromTxLog({ assetName, walletAccount })
101
131
 
102
- const { logItemsByAsset, hasNewTxs, cursorState } = await this.getHistory({
103
- address,
104
- accountState,
105
- walletAccount,
106
- refresh,
107
- })
132
+ const fetchStakingInfo = this.tickCount[walletAccount] % this.ticksBetweenStakeFetches === 0
133
+ const staking = fetchStakingInfo
134
+ ? await this.getStakingInfo({ address, stakingAddresses })
135
+ : accountState.mem
108
136
 
109
- const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
137
+ const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
138
+ const account = await this.getAccount({ address, staking, tokenAccounts })
110
139
 
111
- if (refresh || hasNewTxs || cursorChanged) {
112
- const staking =
113
- refresh || cursorChanged
114
- ? await this.getStakingInfo({ address, stakingAddresses })
115
- : accountState.mem
140
+ const balanceChanged = this.balanceChanged({ account: accountState, newAccount: account })
116
141
 
117
- const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
118
- const account = await this.getAccount({ address, staking, tokenAccounts })
142
+ const isHistoryUpdateTick =
143
+ this.tickCount[walletAccount] % this.ticksBetweenHistoryFetches === 0
119
144
 
145
+ const shouldUpdateHistory = refresh || isHistoryUpdateTick || balanceChanged
146
+ const shouldUpdateOnlyBalance = balanceChanged && !shouldUpdateHistory
147
+ const shouldUpdateBalanceBeforeHistory = true
148
+
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) {
120
151
  // update all state at once
152
+ await this.updateState({ account, walletAccount, staking })
121
153
  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
122
166
  await this.updateTxLogByAsset({ walletAccount, logItemsByAsset, refresh })
123
167
  await this.updateState({ account, cursorState, walletAccount, staking })
168
+ await this.emitUnknownTokensEvent({ tokenAccounts })
124
169
  if (refresh || cursorChanged) {
125
170
  this.cursors[walletAccount] = cursorState.cursor
126
171
  }
@@ -128,7 +173,7 @@ export class SolanaMonitor extends BaseMonitor {
128
173
  }
129
174
 
130
175
  async getHistory({ address, accountState, refresh } = {}) {
131
- let cursor = refresh ? '' : accountState.cursor
176
+ const cursor = refresh ? '' : accountState.cursor
132
177
  const baseAsset = this.asset
133
178
 
134
179
  const { transactions, newCursor } = await this.api.getTransactions(address, {
@@ -181,6 +226,7 @@ export class SolanaMonitor extends BaseMonitor {
181
226
 
182
227
  item.data.meta = tx.data.meta
183
228
  }
229
+
184
230
  if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
185
231
  const feeItem = {
186
232
  ..._.clone(item),
@@ -190,6 +236,7 @@ export class SolanaMonitor extends BaseMonitor {
190
236
  }
191
237
  mappedTransactions.push(feeItem)
192
238
  }
239
+
193
240
  mappedTransactions.push(item)
194
241
  }
195
242
 
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
+ }