@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 +20 -0
- package/package.json +3 -3
- package/src/account-state.js +2 -0
- package/src/api.js +63 -0
- package/src/create-unsigned-tx-for-send.js +71 -9
- package/src/tx-log/clarity-monitor.js +93 -10
- package/src/tx-send.js +1 -0
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.
|
|
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.
|
|
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": "
|
|
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
|
},
|
package/src/account-state.js
CHANGED
|
@@ -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 [
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
(
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
272
|
-
const clientKnownTokens = omitBy(
|
|
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 {
|
|
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