@exodus/solana-api 3.11.8 → 3.12.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [3.12.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.9...@exodus/solana-api@3.12.0) (2025-01-02)
7
+
8
+
9
+ ### Features
10
+
11
+
12
+ * feat: reduce call of Solana getTokenAccountsByOwner (#4762)
13
+
14
+
15
+
16
+ ## [3.11.9](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.8...@exodus/solana-api@3.11.9) (2024-12-31)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+
22
+ * fix: include rentExemptAmount in balance calculation (#4738)
23
+
24
+ * fix: integration tests (#4720)
25
+
26
+
27
+
6
28
  ## [3.11.8](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.6...@exodus/solana-api@3.11.8) (2024-12-10)
7
29
 
8
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.11.8",
3
+ "version": "3.12.0",
4
4
  "description": "Transaction monitors, fee monitors, RPC with the blockchain node, and other networking code for Solana",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -30,7 +30,7 @@
30
30
  "@exodus/fetch": "^1.2.0",
31
31
  "@exodus/models": "^12.0.1",
32
32
  "@exodus/simple-retry": "^0.0.6",
33
- "@exodus/solana-lib": "^3.9.1",
33
+ "@exodus/solana-lib": "^3.9.3",
34
34
  "@exodus/solana-meta": "^2.0.2",
35
35
  "@exodus/timer": "^1.1.1",
36
36
  "bn.js": "^4.11.0",
@@ -47,7 +47,7 @@
47
47
  "@exodus/assets-testing": "^1.0.0",
48
48
  "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
49
49
  },
50
- "gitHead": "46f31ffb8a59f043ca4bd3855c88d6640ca35a1d",
50
+ "gitHead": "70de901b0d9b85f1045949400105203c700fc223",
51
51
  "bugs": {
52
52
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
53
53
  },
@@ -19,6 +19,7 @@ export const createAccountState = ({ assetList }) => {
19
19
  cursor: '',
20
20
  balance: asset.currency.ZERO,
21
21
  tokenBalances: Object.create(null),
22
+ rentExemptAmount: asset.currency.ZERO,
22
23
  stakingInfo: {
23
24
  loaded: false,
24
25
  staking: {
package/src/api.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import BN from 'bn.js'
18
18
  import lodash from 'lodash'
19
19
  import assert from 'minimalistic-assert'
20
+ import ms from 'ms'
20
21
  import urljoin from 'url-join'
21
22
  import wretch from 'wretch'
22
23
 
@@ -46,6 +47,12 @@ export class Api {
46
47
  const result = await this.rpcCall('getTokenSupply', [mintAddress])
47
48
  return result?.value?.amount
48
49
  })
50
+
51
+ this.getMinimumBalanceForRentExemption = memoize(
52
+ (accountSize) => this.rpcCall('getMinimumBalanceForRentExemption', [accountSize]),
53
+ (accountSize) => accountSize,
54
+ ms('15m')
55
+ )
49
56
  }
50
57
 
51
58
  setServer(rpcUrl) {
@@ -179,7 +186,10 @@ export class Api {
179
186
  /**
180
187
  * Get transactions from an address
181
188
  */
182
- async getTransactions(address, { cursor, before, limit, includeUnparsed = false } = {}) {
189
+ async getTransactions(
190
+ address,
191
+ { cursor, before, limit, includeUnparsed = false, tokenAccounts } = Object.create(null)
192
+ ) {
183
193
  limit = limit || this.txsLimit
184
194
  let transactions = []
185
195
  // cursor is a txHash
@@ -187,7 +197,7 @@ export class Api {
187
197
  try {
188
198
  const until = cursor
189
199
 
190
- const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
200
+ const tokenAccountsByOwner = tokenAccounts || (await this.getTokenAccountsByOwner(address)) // Array
191
201
  const tokenAccountAddresses = tokenAccountsByOwner
192
202
  .filter(({ tokenName }) => tokenName !== 'unknown')
193
203
  .map(({ tokenAccountAddress }) => tokenAccountAddress)
@@ -876,10 +886,6 @@ export class Api {
876
886
  }, 0)
877
887
  }
878
888
 
879
- async getMinimumBalanceForRentExemption(size) {
880
- return this.rpcCall('getMinimumBalanceForRentExemption', [size])
881
- }
882
-
883
889
  async getProgramAccounts(programId, config) {
884
890
  return this.rpcCall('getProgramAccounts', [programId, config])
885
891
  }
@@ -33,7 +33,18 @@ export const getBalancesFactory =
33
33
  .clampLowerZero()
34
34
 
35
35
  const total = stakingFeatureAvailable ? balance : balanceWithoutStaking
36
- const spendable = balanceWithoutStaking.sub(asset.accountReserve || zero).clampLowerZero()
36
+
37
+ const networkReserve = accountState.rentExemptAmount || zero
38
+
39
+ const accountReserve = asset.accountReserve || zero
40
+
41
+ // there is no wallet reserve when there are no tokens nor staking actions. Just network reserve for the rent exempt amount.
42
+ const walletReserve =
43
+ hasStakedFunds({ locked, withdrawable, pending }) || hasTokensBalance({ accountState })
44
+ ? accountReserve.sub(networkReserve).clampLowerZero()
45
+ : zero
46
+
47
+ const spendable = balanceWithoutStaking.sub(walletReserve).sub(networkReserve).clampLowerZero()
37
48
 
38
49
  const staked = locked
39
50
  const unstaking = pending
@@ -47,6 +58,8 @@ export const getBalancesFactory =
47
58
  spendable,
48
59
  staked,
49
60
  unstaking,
61
+ networkReserve,
62
+ walletReserve,
50
63
  }
51
64
  }
52
65
 
@@ -93,3 +106,9 @@ const getBalanceFromAccountState = ({ asset, accountState }) => {
93
106
  asset.currency.ZERO
94
107
  )
95
108
  }
109
+
110
+ const hasStakedFunds = ({ locked, withdrawable, pending }) =>
111
+ [locked, withdrawable, pending].some((amount) => amount.isPositive)
112
+
113
+ const hasTokensBalance = ({ accountState }) =>
114
+ Object.values(accountState?.tokenBalances || {}).some((balance) => balance.isPositive)
@@ -195,6 +195,7 @@ export class SolanaMonitor extends BaseMonitor {
195
195
  accountState,
196
196
  walletAccount,
197
197
  refresh,
198
+ tokenAccounts,
198
199
  })
199
200
 
200
201
  const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
@@ -210,7 +211,7 @@ export class SolanaMonitor extends BaseMonitor {
210
211
  }
211
212
  }
212
213
 
213
- async getHistory({ address, accountState, refresh } = {}) {
214
+ async getHistory({ address, accountState, refresh, tokenAccounts } = Object.create(null)) {
214
215
  const cursor = refresh ? '' : accountState.cursor
215
216
  const baseAsset = this.asset
216
217
 
@@ -218,6 +219,7 @@ export class SolanaMonitor extends BaseMonitor {
218
219
  cursor,
219
220
  includeUnparsed: this.includeUnparsed,
220
221
  limit: this.txsLimit,
222
+ tokenAccounts,
221
223
  })
222
224
 
223
225
  const mappedTransactions = []
@@ -227,8 +229,7 @@ export class SolanaMonitor extends BaseMonitor {
227
229
  if (assetName === 'unknown' || !asset) continue // skip unknown tokens
228
230
  const feeAsset = asset.feeAsset
229
231
 
230
- // TODO: remove all uses of toDefault()
231
- const coinAmount = asset.currency.baseUnit(tx.amount).toDefault()
232
+ const coinAmount = asset.currency.baseUnit(tx.amount)
232
233
 
233
234
  const item = {
234
235
  coinName: assetName,
@@ -249,7 +250,7 @@ export class SolanaMonitor extends BaseMonitor {
249
250
  if (tx.owner === address) {
250
251
  // send transaction
251
252
  item.to = tx.to
252
- item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault() // in SOL
253
+ item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
253
254
  item.feeCoinName = baseAsset.name
254
255
  item.coinAmount = item.coinAmount.negate()
255
256
 
@@ -259,7 +260,7 @@ export class SolanaMonitor extends BaseMonitor {
259
260
  }
260
261
  } else if (tx.unparsed) {
261
262
  if (tx.fee !== 0) {
262
- item.feeAmount = baseAsset.currency.baseUnit(tx.fee).toDefault() // in SOL
263
+ item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
263
264
  item.feeCoinName = baseAsset.name
264
265
  }
265
266
 
@@ -294,13 +295,21 @@ export class SolanaMonitor extends BaseMonitor {
294
295
 
295
296
  async getAccount({ refresh, address, tokenAccounts, accountState, walletAccount }) {
296
297
  const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
297
- const [solBalance, splBalances] = await Promise.all([
298
- this.api.getBalance(address),
299
- this.api.getTokensBalance({ address, filterByTokens: tokens, tokenAccounts }),
300
- ])
298
+ const accountInfo = await this.api.getAccountInfo(address).catch(() => {})
299
+ const accountSize = accountInfo?.space || 0
300
+ const solBalance = accountInfo?.lamports || 0
301
+
302
+ const rentExemptValue = await this.api.getMinimumBalanceForRentExemption(accountSize)
303
+ const rentExemptAmount = this.asset.currency.baseUnit(rentExemptValue)
304
+
305
+ const splBalances = await this.api.getTokensBalance({
306
+ address,
307
+ filterByTokens: tokens,
308
+ tokenAccounts,
309
+ })
301
310
 
302
311
  const tokenBalances = _.mapValues(splBalances, (balance, name) =>
303
- this.assets[name].currency.baseUnit(balance).toDefault()
312
+ this.assets[name].currency.baseUnit(balance)
304
313
  )
305
314
 
306
315
  const solBalanceChanged = this.#balanceChanged({
@@ -328,20 +337,26 @@ export class SolanaMonitor extends BaseMonitor {
328
337
  .add(stakedBalance)
329
338
  .add(withdrawableBalance)
330
339
  .add(pendingBalance)
331
- .toDefault()
332
340
 
333
341
  return {
334
342
  account: {
335
343
  balance,
336
344
  tokenBalances,
345
+ rentExemptAmount,
337
346
  },
338
347
  staking,
339
348
  }
340
349
  }
341
350
 
342
351
  async updateState({ account, cursorState, walletAccount, staking }) {
343
- const { balance, tokenBalances } = account
344
- const newData = { balance, tokenBalances, stakingInfo: staking, ...cursorState }
352
+ const { balance, tokenBalances, rentExemptAmount } = account
353
+ const newData = {
354
+ balance,
355
+ rentExemptAmount,
356
+ tokenBalances,
357
+ stakingInfo: staking,
358
+ ...cursorState,
359
+ }
345
360
  return this.updateAccountState({ newData, walletAccount })
346
361
  }
347
362
 
@@ -361,11 +376,11 @@ export class SolanaMonitor extends BaseMonitor {
361
376
  isDelegating: Object.values(stakingInfo.accounts).some(({ state }) =>
362
377
  ['active', 'activating', 'inactive'].includes(state)
363
378
  ), // true if at least 1 account is delegating
364
- locked: this.asset.currency.baseUnit(stakingInfo.locked).toDefault(),
379
+ locked: this.asset.currency.baseUnit(stakingInfo.locked),
365
380
  activating: this.asset.currency.baseUnit(stakingInfo.activating),
366
- withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable).toDefault(),
367
- pending: this.asset.currency.baseUnit(stakingInfo.pending).toDefault(), // still undelegating (not yet available for withdraw)
368
- earned: this.asset.currency.baseUnit(rewards).toDefault(),
381
+ withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable),
382
+ pending: this.asset.currency.baseUnit(stakingInfo.pending), // still undelegating (not yet available for withdraw)
383
+ earned: this.asset.currency.baseUnit(rewards),
369
384
  accounts: stakingInfo.accounts, // Obj
370
385
  }
371
386
  }
package/src/tx-send.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  TOKEN_2022_PROGRAM_ID,
6
6
  TOKEN_PROGRAM_ID,
7
7
  } from '@exodus/solana-lib'
8
- import { transactionToBase58 } from '@exodus/solana-lib/src/tx/common.js'
9
8
  import assert from 'minimalistic-assert'
10
9
 
11
10
  export const createAndBroadcastTXFactory =
@@ -34,6 +33,7 @@ export const createAndBroadcastTXFactory =
34
33
  expectedMintAddress,
35
34
  metadataAddress,
36
35
  creators,
36
+ priorityFee,
37
37
  // </MagicEden>
38
38
  reference,
39
39
  memo,
@@ -129,7 +129,7 @@ export const createAndBroadcastTXFactory =
129
129
  from,
130
130
  to: address,
131
131
  amount,
132
- fee: feeAmount,
132
+ fee: feeData.fee, // feeAmount includes the priortyFee
133
133
  recentBlockhash,
134
134
  feeData,
135
135
  reference,
@@ -139,26 +139,14 @@ export const createAndBroadcastTXFactory =
139
139
  ...magicEdenParams,
140
140
  })
141
141
 
142
- let { priorityFee } = feeData
143
-
144
142
  const transactionForFeeEstimation = prepareForSigning(unsignedTransaction)
145
143
 
146
- if (!priorityFee) {
147
- try {
148
- priorityFee = await api.getPriorityFee(transactionToBase58(transactionForFeeEstimation))
149
- } catch (e) {
150
- console.warn(`Failed to fetch priority fee: ${e.message}`)
151
- priorityFee = feeData.fallbackPriorityFee
152
- }
153
- }
154
-
155
144
  const { unitsConsumed: computeUnits, err } = await api.simulateUnsignedTransaction({
156
145
  message: transactionForFeeEstimation.message,
157
146
  })
147
+ if (err) throw new Error(JSON.stringify(err))
158
148
 
159
- if (err) throw new Error(err)
160
-
161
- unsignedTransaction.txData.priorityFee = priorityFee
149
+ unsignedTransaction.txData.priorityFee = priorityFee ?? 0
162
150
  unsignedTransaction.txData.computeUnits = computeUnits * feeData.computeUnitsMultiplier
163
151
 
164
152
  const { txId, rawTx } = await assetClientInterface.signTransaction({