@exodus/solana-api 3.17.0 → 3.17.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/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
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.17.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.17.1...@exodus/solana-api@3.17.2) (2025-04-21)
7
+
8
+ **Note:** Version bump only for package @exodus/solana-api
9
+
10
+
11
+
12
+
13
+
14
+ ## [3.17.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.17.0...@exodus/solana-api@3.17.1) (2025-04-11)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+
20
+ * fix(solana): don't estimate without fee payer if one is provided (#5435)
21
+
22
+
23
+
6
24
  ## [3.17.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.16.0...@exodus/solana-api@3.17.0) (2025-04-09)
7
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.17.0",
3
+ "version": "3.17.2",
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.7.3",
31
31
  "@exodus/models": "^12.0.1",
32
32
  "@exodus/simple-retry": "^0.0.6",
33
- "@exodus/solana-lib": "^3.10.1",
33
+ "@exodus/solana-lib": "^3.11.1",
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": "6b30927ad36b7a2e802bf9075f04b8dadc7ea0f0",
50
+ "gitHead": "2e550990ef9db4209f8fe4e6e0485579d1afeedf",
51
51
  "bugs": {
52
52
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
53
53
  },
@@ -160,11 +160,23 @@ export const createUnsignedTxForSend = async ({
160
160
  return 150 + CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS
161
161
  }
162
162
 
163
- const transactionForFeeEstimation = prepareForSigning(unsignedTx)
163
+ const transactionForFeeEstimation = await maybeAddFeePayer({
164
+ unsignedTx,
165
+ feePayerApiUrl,
166
+ assetName: asset.baseAsset.name,
167
+ })
168
+ const message = transactionForFeeEstimation.txMeta.usedFeePayer
169
+ ? deserializeTransaction(transactionForFeeEstimation.txData.transactionBuffer).message
170
+ : prepareForSigning(transactionForFeeEstimation).message
171
+
164
172
  const { unitsConsumed, err } = await api.simulateUnsignedTransaction({
165
- message: transactionForFeeEstimation.message,
173
+ message,
166
174
  })
167
- if (err) throw new Error(JSON.stringify(err))
175
+ if (err) {
176
+ // we don't throw error here, because we can use this method to estimate fee
177
+ console.log('SOL estimate unitsConsumed err:', JSON.stringify(err))
178
+ }
179
+
168
180
  return unitsConsumed + CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS
169
181
  }
170
182
 
@@ -216,7 +228,6 @@ export const createUnsignedTxForSend = async ({
216
228
  unsignedTx,
217
229
  feePayerApiUrl,
218
230
  assetName: asset.baseAsset.name,
219
- useFeePayer,
220
231
  })
221
232
  }
222
233
 
@@ -3,19 +3,17 @@ 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
- const DEFAULT_STAKING_RESERVE = 0.01 * 1_000_000_000
7
-
8
6
  export const getBalancesFactory =
9
7
  ({ stakingFeatureAvailable, allowSendingAll }) =>
10
8
  ({ asset, accountState, txLog }) => {
11
9
  const zero = asset.currency.ZERO
12
- const defaultStakingReserve = asset.currency.baseUnit(DEFAULT_STAKING_RESERVE)
13
10
 
14
- const { balance, locked, withdrawable, pending } = fixBalances({
11
+ const { balance, locked, activating, withdrawable, pending } = fixBalances({
15
12
  txLog,
16
13
  balance: getBalanceFromAccountState({ asset, accountState }),
17
14
  locked: accountState.stakingInfo?.locked || zero,
18
15
  withdrawable: accountState.stakingInfo?.withdrawable || zero,
16
+ activating: accountState.stakingInfo?.activating || zero,
19
17
  pending: accountState.stakingInfo?.pending || zero,
20
18
  asset,
21
19
  })
@@ -40,29 +38,21 @@ export const getBalancesFactory =
40
38
 
41
39
  // there is no wallet reserve when there are no tokens nor staking actions. Just network reserve for the rent exempt amount.
42
40
  const needsReserve =
43
- hasStakedFunds({ locked, withdrawable, pending }) || hasTokensBalance({ accountState })
41
+ hasStakedFunds({ locked, activating, withdrawable, pending }) ||
42
+ hasTokensBalance({ accountState })
44
43
 
45
44
  const rentExemptAmountConditional =
46
45
  (accountState.accountSize > 0 ? accountState.rentExemptAmount : zero) || zero
47
46
  const networkReserve = allowSendingAll && !needsReserve ? zero : rentExemptAmountConditional
48
47
 
49
- const accountReserve = asset.accountReserve || zero
50
-
51
- const walletReserve = needsReserve ? accountReserve.sub(networkReserve).clampLowerZero() : zero
52
-
53
- const spendable = balanceWithoutStaking.sub(walletReserve).sub(networkReserve).clampLowerZero()
48
+ const spendable = balanceWithoutStaking.sub(networkReserve).clampLowerZero()
54
49
 
55
- // leave enough sol for staking when the reserve is set to 0
56
- // FIXME: should be able to get total stakeable balance dynamically
57
- // instead of hardcoding a reserve value.
58
- const stakeable = walletReserve.isZero
59
- ? spendable.sub(defaultStakingReserve).clampLowerZero()
60
- : spendable
50
+ const stakeable = spendable
61
51
 
62
52
  const staked = locked
63
53
  const unstaking = pending
64
54
 
65
- const staking = accountState.activating || zero
55
+ const staking = activating || zero
66
56
 
67
57
  return {
68
58
  // legacy
@@ -76,11 +66,19 @@ export const getBalancesFactory =
76
66
  staking,
77
67
  unstaking,
78
68
  networkReserve,
79
- walletReserve,
69
+ walletReserve: zero,
80
70
  }
81
71
  }
82
72
 
83
- const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pending, asset }) => {
73
+ const fixBalances = ({
74
+ txLog = TxSet.EMPTY,
75
+ balance,
76
+ locked,
77
+ withdrawable,
78
+ activating,
79
+ pending,
80
+ asset,
81
+ }) => {
84
82
  for (const tx of txLog) {
85
83
  if ((tx.sent || tx.data.staking) && tx.pending && !tx.error) {
86
84
  if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
@@ -112,6 +110,7 @@ const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pendi
112
110
  balance: balance.clampLowerZero(),
113
111
  locked: locked.clampLowerZero(),
114
112
  withdrawable: withdrawable.clampLowerZero(),
113
+ activating: activating.clampLowerZero(),
115
114
  pending: pending.clampLowerZero(),
116
115
  }
117
116
  }
@@ -124,8 +123,8 @@ const getBalanceFromAccountState = ({ asset, accountState }) => {
124
123
  )
125
124
  }
126
125
 
127
- const hasStakedFunds = ({ locked, withdrawable, pending }) =>
128
- [locked, withdrawable, pending].some((amount) => amount.isPositive)
126
+ const hasStakedFunds = ({ locked, activating, withdrawable, pending }) =>
127
+ [locked, activating, withdrawable, pending].some((amount) => amount.isPositive)
129
128
 
130
129
  const hasTokensBalance = ({ accountState }) =>
131
130
  Object.values(accountState?.tokenBalances || {}).some((balance) => balance.isPositive)
package/src/get-fees.js CHANGED
@@ -1,23 +1,74 @@
1
+ /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
1
2
  import assert from 'minimalistic-assert'
2
3
 
3
4
  import { createUnsignedTxForSend } from './create-unsigned-tx-for-send.js'
4
5
 
6
+ const DEFAULT_RESERVE_FEE = '0.01' // SOL
7
+
5
8
  export const getFeeAsyncFactory = ({ api }) => {
6
9
  assert(api, 'api is required')
7
- return async ({ asset, feeData, unsignedTx: providedUnsignedTx, amount, toAddress, ...rest }) => {
8
- const unsignedTx =
9
- providedUnsignedTx ||
10
- (await createUnsignedTxForSend({
11
- asset,
12
- feeData,
13
- api,
14
- amount: amount ?? asset.currency.baseUnit(1),
15
- toAddress: toAddress ?? rest.fromAddress,
16
- useFeePayer: false,
17
- ...rest,
18
- }))
19
-
20
- return { fee: asset.feeAsset.currency.baseUnit(unsignedTx.txMeta.fee), unsignedTx }
10
+ return async ({
11
+ asset,
12
+ method,
13
+ feeData,
14
+ unsignedTx: providedUnsignedTx,
15
+ amount,
16
+ toAddress,
17
+ stakingInfo,
18
+ ...rest
19
+ }) => {
20
+ let fee, unsignedTx
21
+
22
+ if (providedUnsignedTx) {
23
+ unsignedTx = providedUnsignedTx
24
+ fee = asset.feeAsset.currency.baseUnit(unsignedTx.txMeta.fee)
25
+ } else {
26
+ if (['delegate', 'undelegate', 'withdraw'].includes(method)) {
27
+ assert(stakingInfo, 'stakingInfo is required for staking txs')
28
+ assert(rest.fromAddress, 'fromAddress is required for staking txs')
29
+ assert(feeData, 'feeData is required for staking txs')
30
+
31
+ // staking params
32
+ rest.method = method
33
+ rest.seed = `exodus:${Date.now()}` // unique seed
34
+ rest.pool = stakingInfo.staking.pool
35
+
36
+ const stakeAddresses = []
37
+ for (const [addr, info] of Object.entries(stakingInfo.accounts || {})) {
38
+ if (method === 'undelegate' && (info.state === 'active' || info.state === 'activating'))
39
+ stakeAddresses.push(addr)
40
+ if (method === 'withdraw' && info.state === 'inactive') stakeAddresses.push(addr)
41
+ }
42
+
43
+ rest.stakeAddresses = stakeAddresses
44
+ amount =
45
+ method === 'undelegate'
46
+ ? stakingInfo.locked // unstake all
47
+ : method === 'withdraw'
48
+ ? stakingInfo.withdrawable // withdraw all
49
+ : amount
50
+ }
51
+
52
+ try {
53
+ unsignedTx = await createUnsignedTxForSend({
54
+ asset,
55
+ feeData,
56
+ api,
57
+ amount: amount ?? asset.currency.baseUnit(1),
58
+ toAddress: toAddress ?? rest.fromAddress,
59
+ useFeePayer: false,
60
+ ...rest,
61
+ })
62
+
63
+ fee = asset.feeAsset.currency.baseUnit(unsignedTx.txMeta.fee)
64
+ } catch (err) {
65
+ console.log('error computing right SOL fee:', err)
66
+ // simulating a tx will fail if the user has not enough balance
67
+ fee = asset.feeAsset.currency.defaultUnit(DEFAULT_RESERVE_FEE)
68
+ }
69
+ }
70
+
71
+ return { fee, unsignedTx }
21
72
  }
22
73
  }
23
74
 
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { Api } from './api.js'
6
6
 
7
7
  export { SolanaMonitor } from './tx-log/index.js'
8
8
  export { createAccountState } from './account-state.js'
9
- export { getSolStakedFee, getStakingInfo, getUnstakingFee } from './staking-utils.js'
9
+ export { getSolStakedFee, getStakingInfo } from './staking-utils.js'
10
10
  export {
11
11
  isSolanaStaking,
12
12
  isSolanaUnstaking,
@@ -8,6 +8,9 @@ export const getSolStakedFee = ({ asset, stakingInfo, fee }) => {
8
8
  const { currency } = asset
9
9
  const { accounts } = stakingInfo
10
10
 
11
+ // TODO: REMOVE this method.. only used in Desktop!
12
+ // (also this is wrong, accounts must be filtered by state to compute the right fee)
13
+
11
14
  const allPending = Object.entries(accounts).length
12
15
  return allPending > 0 ? fee.mul(allPending) : currency.ZERO
13
16
  }
@@ -25,8 +28,3 @@ export const getStakingInfo = (stakingInfo) => {
25
28
  earned: stakingInfo.earned,
26
29
  }
27
30
  }
28
-
29
- export const getUnstakingFee = ({ asset, fee, accountState }) => {
30
- const stakingInfo = getStakingInfo(accountState.stakingInfo ?? {})
31
- return getSolStakedFee({ asset, stakingInfo, fee })
32
- }