@exodus/solana-api 3.19.0 → 3.20.1

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,26 @@
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.20.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.20.0...@exodus/solana-api@3.20.1) (2025-06-11)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: SOL owner changed check (#5805)
13
+
14
+
15
+
16
+ ## [3.20.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.19.0...@exodus/solana-api@3.20.0) (2025-06-11)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat(solana): implement unconfirmedSent/unconfirmedReceived in solana balances (#5802)
23
+
24
+
25
+
6
26
  ## [3.19.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.18.2...@exodus/solana-api@3.19.0) (2025-06-10)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.19.0",
3
+ "version": "3.20.1",
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",
@@ -46,7 +46,7 @@
46
46
  "@exodus/assets-testing": "^1.0.0",
47
47
  "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
48
48
  },
49
- "gitHead": "86087d4d05ab6aa603b936c24723e5999f826424",
49
+ "gitHead": "8302af88cf085350c4c0149ab5e581e8297c548b",
50
50
  "bugs": {
51
51
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
52
52
  },
@@ -21,6 +21,7 @@ export const createAccountState = ({ assetList }) => {
21
21
  tokenBalances: Object.create(null),
22
22
  rentExemptAmount: asset.currency.ZERO,
23
23
  accountSize: 0,
24
+ ownerChanged: false,
24
25
  stakingInfo: {
25
26
  loaded: false,
26
27
  staking: {
package/src/api.js CHANGED
@@ -953,6 +953,19 @@ export class Api {
953
953
  return owner && owner !== address
954
954
  }
955
955
 
956
+ async ownerChanged(address, accountInfo) {
957
+ // method to check if the owner of the account has changed, compared to standard programs.
958
+ // as there could be malicious dapps that reassign the ownership of the account (see https://github.com/coinspect/solana-assign-test)
959
+ const value = accountInfo || (await this.getAccountInfo(address))
960
+ const owner = value?.owner // program owner
961
+ if (!owner) return false // not initialized account (or purged)
962
+ return ![
963
+ SYSTEM_PROGRAM_ID.toBase58(),
964
+ TOKEN_PROGRAM_ID.toBase58(),
965
+ TOKEN_2022_PROGRAM_ID.toBase58(),
966
+ ].includes(owner)
967
+ }
968
+
956
969
  ataOwnershipChangedCached = memoizeLruCache(
957
970
  (...args) => this.ataOwnershipChanged(...args),
958
971
  (address, tokenAddress) => `${address}:${tokenAddress}`,
@@ -8,7 +8,15 @@ export const getBalancesFactory =
8
8
  ({ asset, accountState, txLog }) => {
9
9
  const zero = asset.currency.ZERO
10
10
 
11
- const { balance, locked, activating, withdrawable, pending } = fixBalances({
11
+ const {
12
+ balance,
13
+ locked,
14
+ activating,
15
+ withdrawable,
16
+ pending,
17
+ unconfirmedSent,
18
+ unconfirmedReceived,
19
+ } = fixBalances({
12
20
  txLog,
13
21
  balance: getBalanceFromAccountState({ asset, accountState }),
14
22
  locked: accountState.stakingInfo?.locked || zero,
@@ -16,6 +24,8 @@ export const getBalancesFactory =
16
24
  activating: accountState.stakingInfo?.activating || zero,
17
25
  pending: accountState.stakingInfo?.pending || zero,
18
26
  asset,
27
+ unconfirmedSent: zero,
28
+ unconfirmedReceived: zero,
19
29
  })
20
30
  if (asset.baseAsset.name !== asset.name) {
21
31
  return {
@@ -70,6 +80,8 @@ export const getBalancesFactory =
70
80
  unstaking,
71
81
  networkReserve,
72
82
  walletReserve: zero,
83
+ unconfirmedSent,
84
+ unconfirmedReceived,
73
85
  }
74
86
  }
75
87
 
@@ -81,41 +93,56 @@ const fixBalances = ({
81
93
  activating,
82
94
  pending,
83
95
  asset,
96
+ unconfirmedSent,
97
+ unconfirmedReceived,
84
98
  }) => {
85
99
  for (const tx of txLog) {
86
- if ((tx.sent || tx.data.staking) && tx.pending && !tx.error) {
100
+ if (!tx.pending || tx.error) {
101
+ continue
102
+ }
103
+
104
+ if (tx.data.staking) {
105
+ if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
106
+ unconfirmedSent = unconfirmedSent.add(tx.feeAmount)
107
+ }
108
+
109
+ // staking tx
110
+ switch (tx.data.staking?.method) {
111
+ case 'delegate':
112
+ activating = activating.add(tx.coinAmount.abs())
113
+ break
114
+ case 'withdraw':
115
+ withdrawable = asset.currency.ZERO
116
+ break
117
+ case 'undelegate':
118
+ pending = pending.add(locked).add(activating)
119
+ locked = asset.currency.ZERO
120
+ activating = asset.currency.ZERO
121
+ break
122
+ }
123
+ } else if (tx.sent) {
87
124
  if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
88
- balance = balance.sub(tx.feeAmount)
125
+ unconfirmedSent = unconfirmedSent.add(tx.feeAmount)
89
126
  }
90
127
 
91
- if (tx.data.staking) {
92
- // staking tx
93
- switch (tx.data.staking?.method) {
94
- case 'delegate':
95
- activating = activating.add(tx.coinAmount.abs())
96
- break
97
- case 'withdraw':
98
- withdrawable = asset.currency.ZERO
99
- break
100
- case 'undelegate':
101
- pending = pending.add(locked).add(activating)
102
- locked = asset.currency.ZERO
103
- activating = asset.currency.ZERO
104
- break
105
- }
106
- } else {
107
- // coinAmount is negative for sent tx
108
- balance = balance.sub(tx.coinAmount.abs())
128
+ unconfirmedSent = unconfirmedSent.add(tx.coinAmount.abs())
129
+ } else if (tx.received) {
130
+ if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
131
+ unconfirmedReceived = unconfirmedReceived.sub(tx.feeAmount)
109
132
  }
133
+
134
+ unconfirmedReceived = unconfirmedReceived.add(tx.coinAmount.abs())
110
135
  }
111
136
  }
112
137
 
113
138
  return {
114
- balance: balance.clampLowerZero(),
139
+ balance: balance.sub(unconfirmedSent).clampLowerZero(),
115
140
  locked: locked.clampLowerZero(),
116
141
  withdrawable: withdrawable.clampLowerZero(),
117
142
  activating: activating.clampLowerZero(),
118
143
  pending: pending.clampLowerZero(),
144
+ unconfirmedSent,
145
+ unconfirmedReceived,
119
146
  }
120
147
  }
121
148
 
@@ -305,6 +305,8 @@ export class SolanaMonitor extends BaseMonitor {
305
305
  await this.api.getMinimumBalanceForRentExemption(accountSize)
306
306
  )
307
307
 
308
+ const ownerChanged = await this.api.ownerChanged(address, accountInfo)
309
+
308
310
  const tokenBalances = _.mapValues(splBalances, (balance, name) =>
309
311
  this.assets[name].currency.baseUnit(balance)
310
312
  )
@@ -343,6 +345,7 @@ export class SolanaMonitor extends BaseMonitor {
343
345
  tokenBalances,
344
346
  rentExemptAmount,
345
347
  accountSize,
348
+ ownerChanged,
346
349
  },
347
350
  staking,
348
351
  tokenAccounts,
@@ -350,11 +353,12 @@ export class SolanaMonitor extends BaseMonitor {
350
353
  }
351
354
 
352
355
  async updateState({ account, cursorState, walletAccount, staking }) {
353
- const { balance, tokenBalances, rentExemptAmount, accountSize } = account
356
+ const { balance, tokenBalances, rentExemptAmount, accountSize, ownerChanged } = account
354
357
  const newData = {
355
358
  balance,
356
359
  rentExemptAmount,
357
360
  accountSize,
361
+ ownerChanged,
358
362
  tokenBalances,
359
363
  stakingInfo: staking,
360
364
  ...cursorState,