@exodus/solana-api 3.23.0 → 3.24.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.24.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.24.0...@exodus/solana-api@3.24.1) (2025-10-29)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+
12
+ * fix: add missing walletAccount (#6807)
13
+
14
+
15
+
16
+ ## [3.24.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.23.0...@exodus/solana-api@3.24.0) (2025-10-29)
17
+
18
+
19
+ ### Features
20
+
21
+
22
+ * feat: track delegated addresses for solana (#6796)
23
+
24
+
25
+
6
26
  ## [3.23.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.22.1...@exodus/solana-api@3.23.0) (2025-10-27)
7
27
 
8
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "3.23.0",
3
+ "version": "3.24.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",
@@ -33,7 +33,7 @@
33
33
  "@exodus/fetch": "^1.7.3",
34
34
  "@exodus/models": "^12.0.1",
35
35
  "@exodus/simple-retry": "^0.0.6",
36
- "@exodus/solana-lib": "^3.14.0",
36
+ "@exodus/solana-lib": "^3.15.0",
37
37
  "@exodus/solana-meta": "^2.0.2",
38
38
  "@exodus/timer": "^1.1.1",
39
39
  "debug": "^4.1.1",
@@ -49,7 +49,7 @@
49
49
  "@exodus/assets-testing": "^1.0.0",
50
50
  "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
51
51
  },
52
- "gitHead": "a5882a43d39ef9455f25523d20ed3060b40eb1a8",
52
+ "gitHead": "c1a2011b20de8b35640f9b0031b2afbd1abb1605",
53
53
  "bugs": {
54
54
  "url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
55
55
  },
@@ -19,9 +19,11 @@ export const createAccountState = ({ assetList }) => {
19
19
  cursor: '',
20
20
  balance: asset.currency.ZERO,
21
21
  tokenBalances: Object.create(null),
22
+ delegatedTokenAmounts: Object.create(null),
22
23
  rentExemptAmount: asset.currency.ZERO,
23
24
  accountSize: 0,
24
25
  ownerChanged: false,
26
+ delegatedTokenAccounts: [],
25
27
  stakingInfo: {
26
28
  loaded: false,
27
29
  staking: {
package/src/api.js CHANGED
@@ -962,6 +962,69 @@ export class Api {
962
962
  : tokenAccounts
963
963
  }
964
964
 
965
+ /**
966
+ * Get token account states for both owned and delegated accounts
967
+ * @param {string} address - The wallet address (potential delegate)
968
+ * @param {Array} delegatedAccounts - Array of delegated account objects from wallet-accounts
969
+ * @returns {Promise<Array>} Combined list of owned and delegated token accounts
970
+ */
971
+ async getTokenAccountsIncludingDelegated(address, delegatedAccounts = []) {
972
+ // Get owned accounts (existing functionality)
973
+ const ownedAccounts = await this.getTokenAccountsByOwner(address)
974
+
975
+ // Fetch delegated account states
976
+ const delegatedAccountPromises = delegatedAccounts.map(async ({ delegatedAddress }) => {
977
+ try {
978
+ // Fetch the account info to get current balance and delegate status
979
+ const accountInfo = await this.getAccountInfo(delegatedAddress)
980
+
981
+ if (!accountInfo?.data?.parsed) return null
982
+
983
+ const parsedData = accountInfo.data.parsed
984
+ const info = parsedData.info
985
+
986
+ // Verify this account is actually delegated to us
987
+ if (info.delegate !== address) {
988
+ console.warn(`Delegated account ${delegatedAddress} is not delegated to ${address}`)
989
+ return null
990
+ }
991
+
992
+ const mintAddress = info.mint
993
+ const token = this.getTokenByAddress(mintAddress)
994
+ if (!token) return null
995
+
996
+ // Get token fee info if it's a Token2022 token
997
+ const { feeBasisPoints = 0, maximumFee = 0 } =
998
+ accountInfo.owner === TOKEN_2022_PROGRAM_ID.toBase58()
999
+ ? await this.getTokenFeeBasisPoints(mintAddress)
1000
+ : {}
1001
+
1002
+ return {
1003
+ tokenAccountAddress: delegatedAddress,
1004
+ owner: info.owner, // External owner from on-chain data
1005
+ delegate: address, // You are the delegate
1006
+ isDelegated: true,
1007
+ delegatedAmount: info.delegatedAmount?.amount || '0',
1008
+ tokenName: token.name,
1009
+ ticker: token.ticker,
1010
+ balance: info.tokenAmount?.amount || '0',
1011
+ mintAddress,
1012
+ tokenProgram: accountInfo.owner, // TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID
1013
+ decimals: info.tokenAmount?.decimals || token.decimals,
1014
+ feeBasisPoints,
1015
+ maximumFee,
1016
+ }
1017
+ } catch (error) {
1018
+ console.error(`Failed to fetch delegated account ${delegatedAddress}:`, error)
1019
+ return null
1020
+ }
1021
+ })
1022
+
1023
+ const delegatedAccountStates = await Promise.all(delegatedAccountPromises)
1024
+
1025
+ return [...ownedAccounts, ...delegatedAccountStates.filter(Boolean)]
1026
+ }
1027
+
965
1028
  async getTokensBalancesAndAccounts({ address, filterByTokens = [] }) {
966
1029
  const accounts = await this.getTokenAccountsByOwner(address)
967
1030
 
@@ -117,17 +117,79 @@ export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) =
117
117
  throw err
118
118
  }
119
119
 
120
- const [destinationAddressType, isAssociatedTokenAccountActive, fromTokenAccountAddresses] =
121
- await Promise.all([
122
- api.getAddressType(toAddress),
123
- api.isAssociatedTokenAccountActive(tokenAddress),
124
- api.getTokenAccountsByOwner(fromAddress),
125
- ])
126
-
127
- const fromTokenAddresses = fromTokenAccountAddresses.filter(
128
- ({ mintAddress }) => mintAddress === tokenMintAddress
120
+ const [
121
+ destinationAddressType,
122
+ isAssociatedTokenAccountActive,
123
+ fromTokenAccountAddresses,
124
+ accountState,
125
+ ] = await Promise.all([
126
+ api.getAddressType(toAddress),
127
+ api.isAssociatedTokenAccountActive(tokenAddress),
128
+ api.getTokenAccountsByOwner(fromAddress),
129
+ assetClientInterface.getAccountState({
130
+ walletAccount,
131
+ assetName: baseAssetName,
132
+ }),
133
+ ])
134
+
135
+ const delegatedAccounts = accountState?.delegatedTokenAccounts || []
136
+
137
+ // Fetch on-chain state for delegated accounts to get current balance and delegation info
138
+ const delegatedTokenAccountsForMint = await Promise.all(
139
+ delegatedAccounts
140
+ .filter(({ assetName: delegatedAssetName }) => {
141
+ // Get asset from assetClientInterface
142
+ const delegatedAsset = asset.name === delegatedAssetName ? asset : null
143
+ if (!delegatedAsset) return false
144
+ return delegatedAsset.mintAddress === tokenMintAddress
145
+ })
146
+ .map(async (delegatedAccount) => {
147
+ try {
148
+ const accountInfo = await api.rpcCall(
149
+ 'getAccountInfo',
150
+ [delegatedAccount.delegatedAddress, { encoding: 'jsonParsed' }],
151
+ { address: delegatedAccount.delegatedAddress }
152
+ )
153
+
154
+ if (!accountInfo?.value?.data?.parsed) return null
155
+
156
+ const info = accountInfo.value.data.parsed.info
157
+
158
+ // Verify delegation is still active
159
+ if (info.delegate !== fromAddress) {
160
+ console.warn(
161
+ `Delegated account ${delegatedAccount.delegatedAddress} is no longer delegated to ${fromAddress}`
162
+ )
163
+ return null
164
+ }
165
+
166
+ return {
167
+ tokenAccountAddress: delegatedAccount.delegatedAddress,
168
+ mintAddress: tokenMintAddress,
169
+ balance: info.tokenAmount?.amount || '0',
170
+ decimals: info.tokenAmount?.decimals || 0,
171
+ tokenProgram: accountInfo.value.owner,
172
+ isDelegated: true,
173
+ delegatedAmount: info.delegatedAmount?.amount || '0',
174
+ }
175
+ } catch (error) {
176
+ console.error(
177
+ `Failed to fetch delegated account ${delegatedAccount.delegatedAddress}:`,
178
+ error
179
+ )
180
+ return null
181
+ }
182
+ })
129
183
  )
130
184
 
185
+ const validDelegatedAccounts = delegatedTokenAccountsForMint.filter(Boolean)
186
+
187
+ // Combine owned and delegated accounts
188
+ const fromTokenAddresses = [
189
+ ...fromTokenAccountAddresses.filter(({ mintAddress }) => mintAddress === tokenMintAddress),
190
+ ...validDelegatedAccounts,
191
+ ]
192
+
131
193
  tokenParams = {
132
194
  tokenMintAddress,
133
195
  destinationAddressType,
@@ -250,13 +250,84 @@ export class SolanaClarityMonitor extends BaseMonitor {
250
250
 
251
251
  async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
252
252
  const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
253
- const [accountInfo, { balances: splBalances, accounts: tokenAccounts }] = await Promise.all([
254
- this.api.getAccountInfo(address).catch(() => {}),
255
- this.api.getTokensBalancesAndAccounts({
256
- address,
257
- filterByTokens: tokens,
258
- }),
259
- ])
253
+
254
+ const delegatedAccounts = accountState.delegatedTokenAccounts || []
255
+
256
+ const [accountInfo, { balances: splBalances, accounts: ownedTokenAccounts }] =
257
+ await Promise.all([
258
+ this.api.getAccountInfo(address).catch(() => {}),
259
+ this.api.getTokensBalancesAndAccounts({
260
+ address,
261
+ filterByTokens: tokens,
262
+ }),
263
+ ])
264
+
265
+ // Fetch delegated account balances - get full balances first, then delegated amounts
266
+ const delegatedBalances = {}
267
+ const delegatedTokenAmounts = {}
268
+
269
+ for (const { delegatedAddress } of delegatedAccounts) {
270
+ try {
271
+ const accountInfo = await this.api.rpcCall(
272
+ 'getAccountInfo',
273
+ [delegatedAddress, { encoding: 'jsonParsed' }],
274
+ { address: delegatedAddress }
275
+ )
276
+
277
+ if (!accountInfo?.value?.data?.parsed) continue
278
+
279
+ const info = accountInfo.value.data.parsed.info
280
+
281
+ // Verify this account is actually delegated to us
282
+ if (info.delegate !== address) {
283
+ console.warn(`Delegated account ${delegatedAddress} is not delegated to ${address}`)
284
+ continue
285
+ }
286
+
287
+ const mintAddress = info.mint
288
+ const fullBalance = info.tokenAmount?.amount || '0'
289
+ const delegatedAmount = info.delegatedAmount?.amount || '0'
290
+
291
+ // Store full balance for combining with owned balances
292
+ const tokenName = this.api.tokens.get(mintAddress)?.name
293
+ if (!tokenName) continue
294
+ if (this.assets[tokenName]) {
295
+ const fullBalanceCurrency = this.assets[tokenName].currency.baseUnit(fullBalance)
296
+ if (delegatedBalances[mintAddress]) {
297
+ delegatedBalances[mintAddress] = delegatedBalances[mintAddress].add(fullBalanceCurrency)
298
+ } else {
299
+ delegatedBalances[mintAddress] = fullBalanceCurrency
300
+ }
301
+ }
302
+
303
+ // Store delegated amounts separately for the account state
304
+ if (this.assets[tokenName]) {
305
+ const delegatedAmountCurrency = this.assets[tokenName].currency.baseUnit(delegatedAmount)
306
+ if (!delegatedAmountCurrency.isZero()) {
307
+ if (!delegatedTokenAmounts[tokenName]) {
308
+ delegatedTokenAmounts[tokenName] = {}
309
+ }
310
+
311
+ delegatedTokenAmounts[tokenName][delegatedAddress] = delegatedAmountCurrency
312
+ }
313
+ }
314
+ } catch (error) {
315
+ console.error(`Failed to fetch delegated account ${delegatedAddress}:`, error)
316
+ }
317
+ }
318
+
319
+ // Combine owned and delegated balances
320
+ const combinedBalances = { ...splBalances }
321
+ for (const [mintAddress, delegatedBalanceCurrency] of Object.entries(delegatedBalances)) {
322
+ const tokenName = this.api.tokens.get(mintAddress)?.name
323
+ if (tokenName && this.assets[tokenName]) {
324
+ const ownedBalance = this.assets[tokenName].currency.baseUnit(
325
+ combinedBalances[mintAddress] || 0
326
+ )
327
+ const totalBalance = ownedBalance.add(delegatedBalanceCurrency)
328
+ combinedBalances[mintAddress] = Number(totalBalance.toBaseString())
329
+ }
330
+ }
260
331
 
261
332
  const solBalance = accountInfo?.lamports || 0
262
333
 
@@ -268,8 +339,8 @@ export class SolanaClarityMonitor extends BaseMonitor {
268
339
 
269
340
  const ownerChanged = await this.api.ownerChanged(address, accountInfo)
270
341
 
271
- // we can have splBalances for tokens that are not in our asset list
272
- const clientKnownTokens = omitBy(splBalances, (v, mintAddress) => {
342
+ // we can have balances for tokens that are not in our asset list
343
+ const clientKnownTokens = omitBy(combinedBalances, (v, mintAddress) => {
273
344
  const tokenName = this.api.tokens.get(mintAddress)?.name
274
345
  return !this.assets[tokenName]
275
346
  })
@@ -280,6 +351,9 @@ export class SolanaClarityMonitor extends BaseMonitor {
280
351
  })
281
352
  )
282
353
 
354
+ // Use owned token accounts for transaction tracking
355
+ const tokenAccounts = ownedTokenAccounts
356
+
283
357
  const solBalanceChanged = this.#balanceChanged({
284
358
  account: accountState,
285
359
  newAccount: {
@@ -312,6 +386,7 @@ export class SolanaClarityMonitor extends BaseMonitor {
312
386
  account: {
313
387
  balance,
314
388
  tokenBalances,
389
+ delegatedTokenAmounts,
315
390
  rentExemptAmount,
316
391
  accountSize,
317
392
  ownerChanged,
@@ -322,13 +397,21 @@ export class SolanaClarityMonitor extends BaseMonitor {
322
397
  }
323
398
 
324
399
  async updateState({ account, cursorState, walletAccount, staking }) {
325
- const { balance, tokenBalances, rentExemptAmount, accountSize, ownerChanged } = account
400
+ const {
401
+ balance,
402
+ tokenBalances,
403
+ delegatedTokenAmounts,
404
+ rentExemptAmount,
405
+ accountSize,
406
+ ownerChanged,
407
+ } = account
326
408
  const newData = {
327
409
  balance,
328
410
  rentExemptAmount,
329
411
  accountSize,
330
412
  ownerChanged,
331
413
  tokenBalances,
414
+ delegatedTokenAmounts,
332
415
  stakingInfo: staking,
333
416
  ...cursorState,
334
417
  }
package/src/tx-send.js CHANGED
@@ -17,6 +17,7 @@ export const createAndBroadcastTXFactory = ({ api, assetClientInterface }) => {
17
17
 
18
18
  return baseAsset.api.createTx({
19
19
  asset,
20
+ walletAccount,
20
21
  amount: legacyParams.amount,
21
22
  toAddress: legacyParams.address,
22
23
  ...legacyParams,