@exodus/solana-api 3.27.8 → 3.29.0
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 +5 -0
- package/src/api.js +66 -2
- package/src/create-unsigned-tx-for-send.js +45 -8
- package/src/get-balances.js +9 -5
- package/src/index.js +1 -0
- package/src/rpc-api.js +74 -2
- package/src/token-delegation.js +129 -0
- package/src/tx-log/clarity-monitor.js +28 -7
- package/src/tx-log/delegation-utils.js +63 -0
- package/src/tx-log/solana-monitor.js +29 -8
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.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.28.0...@exodus/solana-api@3.29.0) (2026-02-03)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat(solana): add getDelegatedAddresses to full asset (#7377)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.28.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.8...@exodus/solana-api@3.28.0) (2026-02-02)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat(solana): reintroduce solana SPL delegation (#7353)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [3.27.8](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.7...@exodus/solana-api@3.27.8) (2026-01-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.29.0",
|
|
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.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": "d844d6fa5e5eec3c7b279ee5317f676357eb82b2",
|
|
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
|
@@ -37,6 +37,11 @@ export const createAccountState = ({ assetList }) => {
|
|
|
37
37
|
earned: asset.currency.defaultUnit(0),
|
|
38
38
|
accounts: Object.create(null), // stake accounts
|
|
39
39
|
},
|
|
40
|
+
tokenDelegationInfo: {
|
|
41
|
+
loaded: false,
|
|
42
|
+
delegatedAccounts: [],
|
|
43
|
+
delegatedBalances: Object.create(null),
|
|
44
|
+
},
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
static _tokens = [asset, ...tokens] // deprecated - will be removed
|
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,
|
|
@@ -21,6 +23,10 @@ import ms from 'ms'
|
|
|
21
23
|
import urljoin from 'url-join'
|
|
22
24
|
|
|
23
25
|
import { getStakeActivation } from './get-stake-activation/index.js'
|
|
26
|
+
import {
|
|
27
|
+
fetchDelegatedBalances as _fetchDelegatedBalances,
|
|
28
|
+
fetchValidatedDelegation as _fetchValidatedDelegation,
|
|
29
|
+
} from './tx-log/delegation-utils.js'
|
|
24
30
|
import { parseTransaction } from './tx-parser.js'
|
|
25
31
|
import { isSolAddressPoisoningTx } from './txs-utils.js'
|
|
26
32
|
|
|
@@ -67,8 +73,10 @@ export class Api {
|
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
setTokens(assets = {}) {
|
|
70
|
-
|
|
71
|
-
this.tokens = new Map(
|
|
76
|
+
this.tokensByName = pickBy(assets, (asset) => asset.name !== asset.baseAsset.name)
|
|
77
|
+
this.tokens = new Map(
|
|
78
|
+
Object.values(this.tokensByName).map((token) => [token.mintAddress, token])
|
|
79
|
+
)
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
request(path, contentType = 'application/json') {
|
|
@@ -420,6 +428,23 @@ export class Api {
|
|
|
420
428
|
].includes(owner)
|
|
421
429
|
}
|
|
422
430
|
|
|
431
|
+
async fetchValidatedDelegation({ delegatedAddress, expectedDelegate }) {
|
|
432
|
+
return _fetchValidatedDelegation({
|
|
433
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
434
|
+
delegatedAddress,
|
|
435
|
+
expectedDelegate,
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async fetchDelegatedBalances({ delegatedAccounts, address }) {
|
|
440
|
+
return _fetchDelegatedBalances({
|
|
441
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
442
|
+
delegatedAccounts,
|
|
443
|
+
address,
|
|
444
|
+
assets: this.tokensByName,
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
423
448
|
ataOwnershipChangedCached = memoizeLruCache(
|
|
424
449
|
(...args) => this.ataOwnershipChanged(...args),
|
|
425
450
|
(address, tokenAddress) => `${address}:${tokenAddress}`,
|
|
@@ -504,6 +529,45 @@ export class Api {
|
|
|
504
529
|
return lodash.get(value, 'data.parsed.info.mint', null)
|
|
505
530
|
}
|
|
506
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
|
+
|
|
507
571
|
async isTokenAddress(address) {
|
|
508
572
|
const type = await this.getAddressType(address)
|
|
509
573
|
return ['token', 'token-2022'].includes(type)
|
|
@@ -119,17 +119,54 @@ export const createTxFactory = ({ assetClientInterface, api, feePayerClient }) =
|
|
|
119
119
|
throw err
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
const [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
const [
|
|
123
|
+
destinationAddressType,
|
|
124
|
+
isAssociatedTokenAccountActive,
|
|
125
|
+
fromTokenAccountAddresses,
|
|
126
|
+
accountState,
|
|
127
|
+
] = await Promise.all([
|
|
128
|
+
api.getAddressType(toAddress),
|
|
129
|
+
api.isAssociatedTokenAccountActive(tokenAddress),
|
|
130
|
+
api.getTokenAccountsByOwner(fromAddress),
|
|
131
|
+
assetClientInterface.getAccountState({ walletAccount, assetName: baseAssetName }),
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
const ownedForMint = fromTokenAccountAddresses.filter(
|
|
130
135
|
({ mintAddress }) => mintAddress === tokenMintAddress
|
|
131
136
|
)
|
|
132
137
|
|
|
138
|
+
const delegatedAccounts = accountState.tokenDelegationInfo?.delegatedAccounts || []
|
|
139
|
+
const delegatedAccountsResults = await Promise.all(
|
|
140
|
+
delegatedAccounts
|
|
141
|
+
.filter(({ assetName }) => assetName === asset.name)
|
|
142
|
+
.map(async ({ delegatedAddress }) => {
|
|
143
|
+
try {
|
|
144
|
+
const delegation = await api.fetchValidatedDelegation({
|
|
145
|
+
delegatedAddress,
|
|
146
|
+
expectedDelegate: fromAddress,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (!delegation) return null
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
tokenAccountAddress: delegatedAddress,
|
|
153
|
+
mintAddress: tokenMintAddress,
|
|
154
|
+
balance: delegation.balance,
|
|
155
|
+
decimals: delegation.decimals,
|
|
156
|
+
tokenProgram: delegation.tokenProgram,
|
|
157
|
+
isDelegated: true,
|
|
158
|
+
delegatedAmount: delegation.delegatedAmount,
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.warn(`Failed to fetch delegated account ${delegatedAddress}:`, error)
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
const validDelegatedAccounts = delegatedAccountsResults.filter(Boolean)
|
|
167
|
+
|
|
168
|
+
const fromTokenAddresses = [...ownedForMint, ...validDelegatedAccounts]
|
|
169
|
+
|
|
133
170
|
tokenParams = {
|
|
134
171
|
tokenMintAddress,
|
|
135
172
|
destinationAddressType,
|
package/src/get-balances.js
CHANGED
|
@@ -149,11 +149,15 @@ const fixBalances = ({
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const getBalanceFromAccountState = ({ asset, accountState }) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
if (asset.name === asset.baseAsset.name) {
|
|
153
|
+
return accountState.balance || asset.currency.ZERO
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ownedBalance = accountState.tokenBalances?.[asset.name] || asset.currency.ZERO
|
|
157
|
+
const delegatedBalance =
|
|
158
|
+
accountState.tokenDelegationInfo?.delegatedBalances?.[asset.name] || asset.currency.ZERO
|
|
159
|
+
|
|
160
|
+
return ownedBalance.add(delegatedBalance)
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
const hasStakedFunds = ({ locked, activating, withdrawable, pending }) =>
|
package/src/index.js
CHANGED
|
@@ -22,6 +22,7 @@ export { stakingProviderClientFactory } from './staking-provider-client.js'
|
|
|
22
22
|
export { createTxFactory } from './create-unsigned-tx-for-send.js'
|
|
23
23
|
export { feePayerClientFactory } from './fee-payer.js'
|
|
24
24
|
export { createInitAgentWalletFactory } from './init-agent-wallet.js'
|
|
25
|
+
export { createTokenDelegationFactory } from './token-delegation.js'
|
|
25
26
|
|
|
26
27
|
// These are not the same asset objects as the wallet creates, so they should never be returned to the wallet.
|
|
27
28
|
// Initially this may be violated by the Solana code until the first monitor tick updates assets with setTokens()
|
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,
|
|
@@ -17,6 +19,10 @@ import assert from 'minimalistic-assert'
|
|
|
17
19
|
import ms from 'ms'
|
|
18
20
|
|
|
19
21
|
import { getStakeActivation } from './get-stake-activation/index.js'
|
|
22
|
+
import {
|
|
23
|
+
fetchDelegatedBalances as _fetchDelegatedBalances,
|
|
24
|
+
fetchValidatedDelegation as _fetchValidatedDelegation,
|
|
25
|
+
} from './tx-log/delegation-utils.js'
|
|
20
26
|
|
|
21
27
|
const [SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID] = [
|
|
22
28
|
SYSTEM_PROGRAM_ID_KEY.toBase58(),
|
|
@@ -62,8 +68,10 @@ export class RpcApi {
|
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
setTokens(assets) {
|
|
65
|
-
|
|
66
|
-
this.tokens = new Map(
|
|
71
|
+
this.tokensByName = pickBy(assets, (asset) => asset.name !== asset.baseAsset.name)
|
|
72
|
+
this.tokens = new Map(
|
|
73
|
+
Object.values(this.tokensByName).map((token) => [token.mintAddress, token])
|
|
74
|
+
)
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
async rpcCall(method, params = []) {
|
|
@@ -117,6 +125,45 @@ export class RpcApi {
|
|
|
117
125
|
return this.rpcCall('getBlockTime', [slot])
|
|
118
126
|
}
|
|
119
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
|
+
|
|
120
167
|
async waitForTransactionStatus(txIds, status = 'finalized', timeoutMs = ms('1m')) {
|
|
121
168
|
if (!Array.isArray(txIds)) txIds = [txIds]
|
|
122
169
|
const startTime = Date.now()
|
|
@@ -173,6 +220,14 @@ export class RpcApi {
|
|
|
173
220
|
return [TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID].includes(owner)
|
|
174
221
|
}
|
|
175
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
|
+
|
|
176
231
|
async getTokenBalance(tokenAddress) {
|
|
177
232
|
// Returns account balance of a SPL Token account.
|
|
178
233
|
const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
@@ -406,6 +461,23 @@ export class RpcApi {
|
|
|
406
461
|
}
|
|
407
462
|
}
|
|
408
463
|
|
|
464
|
+
async fetchValidatedDelegation({ delegatedAddress, expectedDelegate }) {
|
|
465
|
+
return _fetchValidatedDelegation({
|
|
466
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
467
|
+
delegatedAddress,
|
|
468
|
+
expectedDelegate,
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async fetchDelegatedBalances({ delegatedAccounts, address }) {
|
|
473
|
+
return _fetchDelegatedBalances({
|
|
474
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
475
|
+
delegatedAccounts,
|
|
476
|
+
address,
|
|
477
|
+
assets: this.tokensByName,
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
409
481
|
simulateUnsignedTransaction = async ({ message, transactionMessage }) => {
|
|
410
482
|
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
411
483
|
transactionMessage || message
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createApproveDelegationTx,
|
|
3
|
+
createRevokeDelegationTx,
|
|
4
|
+
TOKEN_2022_PROGRAM_ID,
|
|
5
|
+
TOKEN_PROGRAM_ID,
|
|
6
|
+
} from '@exodus/solana-lib'
|
|
7
|
+
import assert from 'minimalistic-assert'
|
|
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
|
+
|
|
25
|
+
export const createTokenDelegationFactory = ({ api, assetClientInterface }) => {
|
|
26
|
+
assert(api, 'api is required')
|
|
27
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
28
|
+
|
|
29
|
+
const approveDelegation = async ({
|
|
30
|
+
asset,
|
|
31
|
+
walletAccount,
|
|
32
|
+
delegateAddress,
|
|
33
|
+
amount,
|
|
34
|
+
tokenProgram,
|
|
35
|
+
}) => {
|
|
36
|
+
assert(asset, 'asset is required')
|
|
37
|
+
assert(walletAccount, 'walletAccount is required')
|
|
38
|
+
assert(delegateAddress, 'delegateAddress is required')
|
|
39
|
+
|
|
40
|
+
const baseAssetName = asset.baseAsset.name
|
|
41
|
+
const assetName = asset.name
|
|
42
|
+
const tokenMintAddress = asset.mintAddress
|
|
43
|
+
assert(tokenMintAddress, 'asset must be a token with mintAddress')
|
|
44
|
+
|
|
45
|
+
const ownerAddress = await assetClientInterface.getReceiveAddress({
|
|
46
|
+
assetName: baseAssetName,
|
|
47
|
+
walletAccount,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const recentBlockhash = await api.getRecentBlockHash()
|
|
51
|
+
|
|
52
|
+
const transaction = createApproveDelegationTx({
|
|
53
|
+
ownerAddress,
|
|
54
|
+
tokenMintAddress,
|
|
55
|
+
delegateAddress,
|
|
56
|
+
amount,
|
|
57
|
+
recentBlockhash,
|
|
58
|
+
tokenProgram,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
unsignedTx: {
|
|
63
|
+
txData: { transaction },
|
|
64
|
+
txMeta: { assetName },
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const revokeDelegation = async ({ asset, walletAccount, tokenProgram }) => {
|
|
70
|
+
assert(asset, 'asset is required')
|
|
71
|
+
assert(walletAccount, 'walletAccount is required')
|
|
72
|
+
|
|
73
|
+
const baseAssetName = asset.baseAsset.name
|
|
74
|
+
const assetName = asset.name
|
|
75
|
+
const tokenMintAddress = asset.mintAddress
|
|
76
|
+
assert(tokenMintAddress, 'asset must be a token with mintAddress')
|
|
77
|
+
|
|
78
|
+
const ownerAddress = await assetClientInterface.getReceiveAddress({
|
|
79
|
+
assetName: baseAssetName,
|
|
80
|
+
walletAccount,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const recentBlockhash = await api.getRecentBlockHash()
|
|
84
|
+
|
|
85
|
+
const transaction = createRevokeDelegationTx({
|
|
86
|
+
ownerAddress,
|
|
87
|
+
tokenMintAddress,
|
|
88
|
+
recentBlockhash,
|
|
89
|
+
tokenProgram,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
unsignedTx: {
|
|
94
|
+
txData: { transaction },
|
|
95
|
+
txMeta: { assetName },
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
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 }
|
|
129
|
+
}
|
|
@@ -271,12 +271,18 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
address,
|
|
278
|
-
|
|
279
|
-
|
|
274
|
+
const delegatedAccounts = accountState.tokenDelegationInfo?.delegatedAccounts || []
|
|
275
|
+
const [accountInfo, { balances: splBalances, accounts: tokenAccounts }, delegatedBalances] =
|
|
276
|
+
await Promise.all([
|
|
277
|
+
this.clarityApi.getAccountInfo(address).catch(() => {}),
|
|
278
|
+
this.clarityApi.getTokensBalancesAndAccounts({
|
|
279
|
+
address,
|
|
280
|
+
}),
|
|
281
|
+
this.clarityApi.fetchDelegatedBalances({
|
|
282
|
+
delegatedAccounts,
|
|
283
|
+
address,
|
|
284
|
+
}),
|
|
285
|
+
])
|
|
280
286
|
|
|
281
287
|
const solBalance = accountInfo?.lamports || 0
|
|
282
288
|
|
|
@@ -332,6 +338,8 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
332
338
|
account: {
|
|
333
339
|
balance,
|
|
334
340
|
tokenBalances,
|
|
341
|
+
delegatedAccounts,
|
|
342
|
+
delegatedBalances,
|
|
335
343
|
rentExemptAmount,
|
|
336
344
|
accountSize,
|
|
337
345
|
ownerChanged,
|
|
@@ -343,13 +351,26 @@ export class SolanaClarityMonitor extends BaseMonitor {
|
|
|
343
351
|
|
|
344
352
|
updateState({ account, cursorState = {}, walletAccount, staking, batch }) {
|
|
345
353
|
const assetName = this.asset.name
|
|
346
|
-
const {
|
|
354
|
+
const {
|
|
355
|
+
balance,
|
|
356
|
+
tokenBalances,
|
|
357
|
+
delegatedAccounts,
|
|
358
|
+
delegatedBalances,
|
|
359
|
+
rentExemptAmount,
|
|
360
|
+
accountSize,
|
|
361
|
+
ownerChanged,
|
|
362
|
+
} = account
|
|
347
363
|
const newData = {
|
|
348
364
|
balance,
|
|
349
365
|
rentExemptAmount,
|
|
350
366
|
accountSize,
|
|
351
367
|
ownerChanged,
|
|
352
368
|
tokenBalances,
|
|
369
|
+
tokenDelegationInfo: {
|
|
370
|
+
loaded: true,
|
|
371
|
+
delegatedAccounts: delegatedAccounts || [],
|
|
372
|
+
delegatedBalances: delegatedBalances || Object.create(null),
|
|
373
|
+
},
|
|
353
374
|
stakingInfo: staking,
|
|
354
375
|
...cursorState,
|
|
355
376
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
async function fetchDelegatedAccountInfo({ rpcCall, delegatedAddress }) {
|
|
2
|
+
return rpcCall('getAccountInfo', [delegatedAddress, { encoding: 'jsonParsed' }], {
|
|
3
|
+
address: delegatedAddress,
|
|
4
|
+
})
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function parseDelegationInfo({ accountInfo, expectedDelegate }) {
|
|
8
|
+
if (!accountInfo?.value?.data?.parsed) return null
|
|
9
|
+
|
|
10
|
+
const info = accountInfo.value.data.parsed.info
|
|
11
|
+
if (info.delegate !== expectedDelegate) return null
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
balance: info.tokenAmount?.amount || '0',
|
|
15
|
+
decimals: info.tokenAmount?.decimals || 0,
|
|
16
|
+
delegatedAmount: info.delegatedAmount?.amount || '0',
|
|
17
|
+
tokenProgram: accountInfo.value.owner,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function calculateSpendableAmount({ balance, delegatedAmount }) {
|
|
22
|
+
return BigInt(balance) < BigInt(delegatedAmount) ? balance : delegatedAmount
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function fetchValidatedDelegation({ rpcCall, delegatedAddress, expectedDelegate }) {
|
|
26
|
+
const accountInfo = await fetchDelegatedAccountInfo({ rpcCall, delegatedAddress })
|
|
27
|
+
return parseDelegationInfo({ accountInfo, expectedDelegate })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function fetchDelegatedBalances({ delegatedAccounts, address, assets, rpcCall }) {
|
|
31
|
+
const delegatedBalances = Object.create(null)
|
|
32
|
+
|
|
33
|
+
if (delegatedAccounts.length === 0) return delegatedBalances
|
|
34
|
+
|
|
35
|
+
for (const { delegatedAddress, assetName } of delegatedAccounts) {
|
|
36
|
+
try {
|
|
37
|
+
const delegation = await fetchValidatedDelegation({
|
|
38
|
+
rpcCall,
|
|
39
|
+
delegatedAddress,
|
|
40
|
+
expectedDelegate: address,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (!delegation) continue
|
|
44
|
+
if (!assets[assetName]) continue
|
|
45
|
+
|
|
46
|
+
const spendable = calculateSpendableAmount({
|
|
47
|
+
balance: delegation.balance,
|
|
48
|
+
delegatedAmount: delegation.delegatedAmount,
|
|
49
|
+
})
|
|
50
|
+
const amount = assets[assetName].currency.baseUnit(spendable)
|
|
51
|
+
|
|
52
|
+
if (delegatedBalances[assetName]) {
|
|
53
|
+
delegatedBalances[assetName] = delegatedBalances[assetName].add(amount)
|
|
54
|
+
} else {
|
|
55
|
+
delegatedBalances[assetName] = amount
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Failed to fetch delegated account ${delegatedAddress}:`, error)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return delegatedBalances
|
|
63
|
+
}
|
|
@@ -254,14 +254,20 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
257
|
+
const delegatedAccounts = accountState.tokenDelegationInfo?.delegatedAccounts || []
|
|
257
258
|
const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
|
|
258
|
-
const [accountInfo, { balances: splBalances, accounts: tokenAccounts }] =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
259
|
+
const [accountInfo, { balances: splBalances, accounts: tokenAccounts }, delegatedBalances] =
|
|
260
|
+
await Promise.all([
|
|
261
|
+
this.rpcApi.getAccountInfo(address).catch(() => {}),
|
|
262
|
+
this.rpcApi.getTokensBalancesAndAccounts({
|
|
263
|
+
address,
|
|
264
|
+
filterByTokens: tokens,
|
|
265
|
+
}),
|
|
266
|
+
this.rpcApi.fetchDelegatedBalances({
|
|
267
|
+
delegatedAccounts,
|
|
268
|
+
address,
|
|
269
|
+
}),
|
|
270
|
+
])
|
|
265
271
|
|
|
266
272
|
const solBalance = accountInfo?.lamports || 0
|
|
267
273
|
|
|
@@ -309,6 +315,8 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
309
315
|
account: {
|
|
310
316
|
balance,
|
|
311
317
|
tokenBalances,
|
|
318
|
+
delegatedAccounts,
|
|
319
|
+
delegatedBalances,
|
|
312
320
|
rentExemptAmount,
|
|
313
321
|
accountSize,
|
|
314
322
|
ownerChanged,
|
|
@@ -320,13 +328,26 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
320
328
|
|
|
321
329
|
updateState({ account, cursorState = {}, walletAccount, staking, batch }) {
|
|
322
330
|
const assetName = this.asset.name
|
|
323
|
-
const {
|
|
331
|
+
const {
|
|
332
|
+
balance,
|
|
333
|
+
tokenBalances,
|
|
334
|
+
delegatedAccounts,
|
|
335
|
+
delegatedBalances,
|
|
336
|
+
rentExemptAmount,
|
|
337
|
+
accountSize,
|
|
338
|
+
ownerChanged,
|
|
339
|
+
} = account
|
|
324
340
|
const newData = {
|
|
325
341
|
balance,
|
|
326
342
|
rentExemptAmount,
|
|
327
343
|
accountSize,
|
|
328
344
|
ownerChanged,
|
|
329
345
|
tokenBalances,
|
|
346
|
+
tokenDelegationInfo: {
|
|
347
|
+
loaded: true,
|
|
348
|
+
delegatedAccounts: delegatedAccounts || [],
|
|
349
|
+
delegatedBalances: delegatedBalances || Object.create(null),
|
|
350
|
+
},
|
|
330
351
|
stakingInfo: staking,
|
|
331
352
|
...cursorState,
|
|
332
353
|
}
|