@exodus/solana-api 3.28.0 → 3.29.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/api.js +41 -0
- package/src/rpc-api.js +49 -0
- package/src/token-delegation.js +51 -2
- package/src/tx-log/clarity-monitor.js +4 -11
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.29.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.0...@exodus/solana-api@3.29.1) (2026-02-03)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: SPL balances regression (#7388)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.29.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.28.0...@exodus/solana-api@3.29.0) (2026-02-03)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat(solana): add getDelegatedAddresses to full asset (#7377)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [3.28.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.8...@exodus/solana-api@3.28.0) (2026-02-02)
|
|
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.29.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.20.
|
|
36
|
+
"@exodus/solana-lib": "^3.20.1",
|
|
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": "3679676ff106c29f167cab7f96d3ce8957299f9c",
|
|
53
53
|
"bugs": {
|
|
54
54
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
55
55
|
},
|
package/src/api.js
CHANGED
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
buildRawTransaction,
|
|
8
8
|
computeBalance,
|
|
9
9
|
deserializeMetaplexMetadata,
|
|
10
|
+
EXOD_SHARES_MINT_ADDRESS,
|
|
10
11
|
filterAccountsByOwner,
|
|
12
|
+
findAssociatedTokenAddress,
|
|
11
13
|
getMetadataAccount,
|
|
12
14
|
getTransactionSimulationParams,
|
|
13
15
|
SOL_DECIMAL,
|
|
@@ -527,6 +529,45 @@ export class Api {
|
|
|
527
529
|
return lodash.get(value, 'data.parsed.info.mint', null)
|
|
528
530
|
}
|
|
529
531
|
|
|
532
|
+
async isWhitelisted({ address, tokenMintAddress = EXOD_SHARES_MINT_ADDRESS }) {
|
|
533
|
+
// check if address is whitelisted for a specific token (e.g. EXOD shares)
|
|
534
|
+
// Returns true if the ATA exists, is not frozen, and can receive the token
|
|
535
|
+
try {
|
|
536
|
+
// Derive the associated token account address for the given owner and mint
|
|
537
|
+
// EXOD is a Token-2022, so we use TOKEN_2022_PROGRAM_ID
|
|
538
|
+
const associatedTokenAddress = findAssociatedTokenAddress(
|
|
539
|
+
address,
|
|
540
|
+
tokenMintAddress,
|
|
541
|
+
TOKEN_2022_PROGRAM_ID.toBase58()
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
// Get the token account info to check if it exists and its state
|
|
545
|
+
const accountInfo = await this.getAccountInfo(associatedTokenAddress)
|
|
546
|
+
|
|
547
|
+
// If account doesn't exist, it's not whitelisted
|
|
548
|
+
if (!accountInfo || !accountInfo.data) {
|
|
549
|
+
return false
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const parsedInfo = accountInfo.data?.parsed?.info
|
|
553
|
+
|
|
554
|
+
// Check if the mint matches
|
|
555
|
+
if (parsedInfo?.mint !== tokenMintAddress) {
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check if the account is frozen
|
|
560
|
+
// State can be "initialized" (not frozen) or "frozen"
|
|
561
|
+
const state = parsedInfo?.state
|
|
562
|
+
|
|
563
|
+
// Account exists, mint matches, and is not frozen - whitelisted
|
|
564
|
+
return state !== 'frozen'
|
|
565
|
+
} catch (error) {
|
|
566
|
+
console.warn('isWhitelisted error:', error)
|
|
567
|
+
return false
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
530
571
|
async isTokenAddress(address) {
|
|
531
572
|
const type = await this.getAddressType(address)
|
|
532
573
|
return ['token', 'token-2022'].includes(type)
|
package/src/rpc-api.js
CHANGED
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
buildRawTransaction,
|
|
6
6
|
computeBalance,
|
|
7
7
|
deserializeMetaplexMetadata,
|
|
8
|
+
EXOD_SHARES_MINT_ADDRESS,
|
|
8
9
|
filterAccountsByOwner,
|
|
10
|
+
findAssociatedTokenAddress,
|
|
9
11
|
getMetadataAccount,
|
|
10
12
|
getTransactionSimulationParams,
|
|
11
13
|
SOL_DECIMAL,
|
|
@@ -123,6 +125,45 @@ export class RpcApi {
|
|
|
123
125
|
return this.rpcCall('getBlockTime', [slot])
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
async isWhitelisted({ address, tokenMintAddress = EXOD_SHARES_MINT_ADDRESS }) {
|
|
129
|
+
// check if address is whitelisted for a specific token (e.g. EXOD shares)
|
|
130
|
+
// Returns true if the ATA exists, is not frozen, and can receive the token
|
|
131
|
+
try {
|
|
132
|
+
// Derive the associated token account address for the given owner and mint
|
|
133
|
+
// EXOD is a Token-2022, so we use TOKEN_2022_PROGRAM_ID
|
|
134
|
+
const associatedTokenAddress = findAssociatedTokenAddress(
|
|
135
|
+
address,
|
|
136
|
+
tokenMintAddress,
|
|
137
|
+
TOKEN_2022_PROGRAM_ID
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Get the token account info to check if it exists and its state
|
|
141
|
+
const accountInfo = await this.getRawAccountInfo({ address: associatedTokenAddress })
|
|
142
|
+
|
|
143
|
+
// If account doesn't exist, it's not whitelisted
|
|
144
|
+
if (!accountInfo || !accountInfo.data) {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const parsedInfo = accountInfo.data?.parsed?.info
|
|
149
|
+
|
|
150
|
+
// Check if the mint matches
|
|
151
|
+
if (parsedInfo?.mint !== tokenMintAddress) {
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if the account is frozen
|
|
156
|
+
// State can be "initialized" (not frozen) or "frozen"
|
|
157
|
+
const state = parsedInfo?.state
|
|
158
|
+
|
|
159
|
+
// Account exists, mint matches, and is not frozen - whitelisted
|
|
160
|
+
return state !== 'frozen'
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.warn('isWhitelisted error:', error)
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
126
167
|
async waitForTransactionStatus(txIds, status = 'finalized', timeoutMs = ms('1m')) {
|
|
127
168
|
if (!Array.isArray(txIds)) txIds = [txIds]
|
|
128
169
|
const startTime = Date.now()
|
|
@@ -179,6 +220,14 @@ export class RpcApi {
|
|
|
179
220
|
return [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].includes(owner)
|
|
180
221
|
}
|
|
181
222
|
|
|
223
|
+
async getRawAccountInfo({ address, encoding = 'jsonParsed' }) {
|
|
224
|
+
const { value } = await this.rpcCall('getAccountInfo', [
|
|
225
|
+
address,
|
|
226
|
+
{ encoding, commitment: 'confirmed' },
|
|
227
|
+
])
|
|
228
|
+
return value
|
|
229
|
+
}
|
|
230
|
+
|
|
182
231
|
async getTokenBalance(tokenAddress) {
|
|
183
232
|
// Returns account balance of a SPL Token account.
|
|
184
233
|
const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
package/src/token-delegation.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createApproveDelegationTx,
|
|
3
|
+
createRevokeDelegationTx,
|
|
4
|
+
TOKEN_2022_PROGRAM_ID,
|
|
5
|
+
TOKEN_PROGRAM_ID,
|
|
6
|
+
} from '@exodus/solana-lib'
|
|
2
7
|
import assert from 'minimalistic-assert'
|
|
3
8
|
|
|
9
|
+
// SPL Token account layout offsets
|
|
10
|
+
const MINT_OFFSET = 0
|
|
11
|
+
const DELEGATE_OFFSET = 76 // mint (32) + owner (32) + amount (8) + delegateOption (4)
|
|
12
|
+
|
|
13
|
+
async function fetchDelegatedAccountsForMint({ api, programId, mintAddress, delegateAddress }) {
|
|
14
|
+
const config = {
|
|
15
|
+
filters: [
|
|
16
|
+
{ memcmp: { offset: MINT_OFFSET, bytes: mintAddress } },
|
|
17
|
+
{ memcmp: { offset: DELEGATE_OFFSET, bytes: delegateAddress } },
|
|
18
|
+
],
|
|
19
|
+
encoding: 'jsonParsed',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return api.getProgramAccounts(programId, config)
|
|
23
|
+
}
|
|
24
|
+
|
|
4
25
|
export const createTokenDelegationFactory = ({ api, assetClientInterface }) => {
|
|
5
26
|
assert(api, 'api is required')
|
|
6
27
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
@@ -76,5 +97,33 @@ export const createTokenDelegationFactory = ({ api, assetClientInterface }) => {
|
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
const getDelegatedAddresses = async ({ address, mintAddress }) => {
|
|
101
|
+
assert(address, 'address is required')
|
|
102
|
+
assert(mintAddress, 'mintAddress is required')
|
|
103
|
+
|
|
104
|
+
const token = api.tokens.get(mintAddress)
|
|
105
|
+
if (!token) return []
|
|
106
|
+
|
|
107
|
+
const [tokenAccounts, token2022Accounts] = await Promise.all([
|
|
108
|
+
fetchDelegatedAccountsForMint({
|
|
109
|
+
api,
|
|
110
|
+
programId: TOKEN_PROGRAM_ID.toBase58(),
|
|
111
|
+
mintAddress,
|
|
112
|
+
delegateAddress: address,
|
|
113
|
+
}),
|
|
114
|
+
fetchDelegatedAccountsForMint({
|
|
115
|
+
api,
|
|
116
|
+
programId: TOKEN_2022_PROGRAM_ID.toBase58(),
|
|
117
|
+
mintAddress,
|
|
118
|
+
delegateAddress: address,
|
|
119
|
+
}),
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
return [...tokenAccounts, ...token2022Accounts].map((account) => ({
|
|
123
|
+
delegatedAddress: account.pubkey,
|
|
124
|
+
assetName: token.name,
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { approveDelegation, revokeDelegation, getDelegatedAddresses }
|
|
80
129
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseMonitor } from '@exodus/asset-lib'
|
|
2
|
-
import {
|
|
2
|
+
import { mapValues, pickBy } from '@exodus/basic-utils'
|
|
3
3
|
import lodash from 'lodash'
|
|
4
4
|
import assert from 'minimalistic-assert'
|
|
5
5
|
import ms from 'ms'
|
|
@@ -294,16 +294,9 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
294
294
|
|
|
295
295
|
const ownerChanged = await this.clarityApi.ownerChanged(address, accountInfo)
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return !this.assets[tokenName]
|
|
301
|
-
})
|
|
302
|
-
const tokenBalances = Object.fromEntries(
|
|
303
|
-
Object.entries(clientKnownTokens).map(([mintAddress, balance]) => {
|
|
304
|
-
const tokenName = this.clarityApi.tokens.get(mintAddress)?.name
|
|
305
|
-
return [tokenName, this.assets[tokenName].currency.baseUnit(balance)]
|
|
306
|
-
})
|
|
297
|
+
const tokenBalances = mapValues(
|
|
298
|
+
pickBy(splBalances, (_balance, name) => this.assets[name]), // filter unknown tokens
|
|
299
|
+
(balance, name) => this.assets[name].currency.baseUnit(balance)
|
|
307
300
|
)
|
|
308
301
|
|
|
309
302
|
const solBalanceChanged = this.#balanceChanged({
|