@exodus/solana-api 3.27.7 → 3.28.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 +25 -2
- package/src/clarity-api.js +77 -3
- 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 +25 -2
- package/src/token-delegation.js +80 -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.28.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.8...@exodus/solana-api@3.28.0) (2026-02-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat(solana): reintroduce solana SPL delegation (#7353)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.27.8](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.7...@exodus/solana-api@3.27.8) (2026-01-27)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: Solana Normalize Clarity API responses for contract compatibility (#7334)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [3.27.7](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.27.6...@exodus/solana-api@3.27.7) (2026-01-26)
|
|
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.28.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.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": "3cea3bc89ff5c4aaa461ac148e384ea24dbe2d38",
|
|
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
|
@@ -21,6 +21,10 @@ import ms from 'ms'
|
|
|
21
21
|
import urljoin from 'url-join'
|
|
22
22
|
|
|
23
23
|
import { getStakeActivation } from './get-stake-activation/index.js'
|
|
24
|
+
import {
|
|
25
|
+
fetchDelegatedBalances as _fetchDelegatedBalances,
|
|
26
|
+
fetchValidatedDelegation as _fetchValidatedDelegation,
|
|
27
|
+
} from './tx-log/delegation-utils.js'
|
|
24
28
|
import { parseTransaction } from './tx-parser.js'
|
|
25
29
|
import { isSolAddressPoisoningTx } from './txs-utils.js'
|
|
26
30
|
|
|
@@ -67,8 +71,10 @@ export class Api {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
setTokens(assets = {}) {
|
|
70
|
-
|
|
71
|
-
this.tokens = new Map(
|
|
74
|
+
this.tokensByName = pickBy(assets, (asset) => asset.name !== asset.baseAsset.name)
|
|
75
|
+
this.tokens = new Map(
|
|
76
|
+
Object.values(this.tokensByName).map((token) => [token.mintAddress, token])
|
|
77
|
+
)
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
request(path, contentType = 'application/json') {
|
|
@@ -420,6 +426,23 @@ export class Api {
|
|
|
420
426
|
].includes(owner)
|
|
421
427
|
}
|
|
422
428
|
|
|
429
|
+
async fetchValidatedDelegation({ delegatedAddress, expectedDelegate }) {
|
|
430
|
+
return _fetchValidatedDelegation({
|
|
431
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
432
|
+
delegatedAddress,
|
|
433
|
+
expectedDelegate,
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async fetchDelegatedBalances({ delegatedAccounts, address }) {
|
|
438
|
+
return _fetchDelegatedBalances({
|
|
439
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
440
|
+
delegatedAccounts,
|
|
441
|
+
address,
|
|
442
|
+
assets: this.tokensByName,
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
423
446
|
ataOwnershipChangedCached = memoizeLruCache(
|
|
424
447
|
(...args) => this.ataOwnershipChanged(...args),
|
|
425
448
|
(address, tokenAddress) => `${address}:${tokenAddress}`,
|
package/src/clarity-api.js
CHANGED
|
@@ -13,6 +13,8 @@ const cleanQuery = (obj) => omitBy(obj, (v) => v === undefined)
|
|
|
13
13
|
|
|
14
14
|
// Tokens + SOL api support
|
|
15
15
|
export class ClarityApi extends RpcApi {
|
|
16
|
+
// NOTE: getTokenByAddress(mint) is inherited from RpcApi (uses this.tokens Map)
|
|
17
|
+
|
|
16
18
|
getSupply = memoize(async (mintAddress) => {
|
|
17
19
|
// cached getSupply
|
|
18
20
|
return this.request(`/util/get-token-supply/${encodeURIComponent(mintAddress)}`)
|
|
@@ -45,7 +47,7 @@ export class ClarityApi extends RpcApi {
|
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
async getTransactions(address, { cursor, limit, includeUnparsed = false } = Object.create(null)) {
|
|
48
|
-
|
|
50
|
+
const result = await this.request(`/addresses/${encodeURIComponent(address)}/transactions`)
|
|
49
51
|
.query(
|
|
50
52
|
cleanQuery({
|
|
51
53
|
cursor,
|
|
@@ -55,6 +57,33 @@ export class ClarityApi extends RpcApi {
|
|
|
55
57
|
)
|
|
56
58
|
.get()
|
|
57
59
|
.json()
|
|
60
|
+
|
|
61
|
+
// Normalize token metadata: fill in tokenName, ticker, decimals from assets registry if missing.
|
|
62
|
+
// This ensures compatibility with consumers that expect these fields populated.
|
|
63
|
+
let transactions
|
|
64
|
+
if (Array.isArray(result?.transactions)) {
|
|
65
|
+
transactions = result.transactions.map((tx) => {
|
|
66
|
+
const mintAddress = tx?.token?.mintAddress
|
|
67
|
+
if (!mintAddress) return tx
|
|
68
|
+
|
|
69
|
+
const token = this.getTokenByAddress(mintAddress)
|
|
70
|
+
if (!token) return tx
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...tx,
|
|
74
|
+
token: {
|
|
75
|
+
...tx.token,
|
|
76
|
+
tokenName: tx.token.tokenName ?? token.name,
|
|
77
|
+
ticker: tx.token.ticker ?? token.ticker,
|
|
78
|
+
decimals: tx.token.decimals ?? token.decimals,
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
} else {
|
|
83
|
+
transactions = result?.transactions
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { ...result, transactions }
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
async getTransactionById(txId) {
|
|
@@ -63,10 +92,55 @@ export class ClarityApi extends RpcApi {
|
|
|
63
92
|
.json()
|
|
64
93
|
}
|
|
65
94
|
|
|
66
|
-
async getTokensBalancesAndAccounts({ address }) {
|
|
67
|
-
|
|
95
|
+
async getTokensBalancesAndAccounts({ address, filterByTokens } = Object.create(null)) {
|
|
96
|
+
const result = await this.request(`/addresses/${encodeURIComponent(address)}/tokens-balance`)
|
|
68
97
|
.get()
|
|
69
98
|
.json()
|
|
99
|
+
|
|
100
|
+
const rawAccounts = Array.isArray(result?.accounts) ? result.accounts : []
|
|
101
|
+
const rawBalances =
|
|
102
|
+
result?.balances && typeof result.balances === 'object' ? result.balances : {}
|
|
103
|
+
|
|
104
|
+
// Normalize accounts: add tokenName, ticker, decimals, and standard field names.
|
|
105
|
+
const accounts = rawAccounts.map((account) => {
|
|
106
|
+
const mintAddress = account?.mintAddress ?? account?.mint
|
|
107
|
+
const token = mintAddress ? this.getTokenByAddress(mintAddress) : null
|
|
108
|
+
const tokenProgram =
|
|
109
|
+
account?.tokenProgram ??
|
|
110
|
+
account?.program ??
|
|
111
|
+
token?.tokenProgram ??
|
|
112
|
+
TOKEN_PROGRAM_ID.toBase58()
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
tokenAccountAddress: account?.tokenAccountAddress ?? account?.address ?? account?.pubkey,
|
|
116
|
+
owner: account?.owner ?? address,
|
|
117
|
+
tokenName: token?.name ?? 'unknown',
|
|
118
|
+
ticker: token?.ticker ?? 'UNKNOWN',
|
|
119
|
+
balance: account?.balance ?? account?.amount ?? rawBalances[mintAddress] ?? '0',
|
|
120
|
+
mintAddress,
|
|
121
|
+
tokenProgram,
|
|
122
|
+
decimals: token?.decimals ?? 0,
|
|
123
|
+
feeBasisPoints: account?.feeBasisPoints ?? 0,
|
|
124
|
+
maximumFee: account?.maximumFee ?? 0,
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Normalize balances: map from mint address keys to asset name keys.
|
|
129
|
+
// This ensures compatibility with existing callers that expect `{ serum: 100 }` shape.
|
|
130
|
+
const balances = Object.create(null)
|
|
131
|
+
for (const [mintAddress, amount] of Object.entries(rawBalances)) {
|
|
132
|
+
const token = this.getTokenByAddress(mintAddress)
|
|
133
|
+
if (!token) continue
|
|
134
|
+
balances[token.name] = Number(amount)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (Array.isArray(filterByTokens) && filterByTokens.length > 0) {
|
|
138
|
+
for (const key of Object.keys(balances)) {
|
|
139
|
+
if (!filterByTokens.includes(key)) delete balances[key]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { balances, accounts }
|
|
70
144
|
}
|
|
71
145
|
|
|
72
146
|
async getTokenAccountsByOwner(address, tokenTicker) {
|
|
@@ -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
|
@@ -17,6 +17,10 @@ import assert from 'minimalistic-assert'
|
|
|
17
17
|
import ms from 'ms'
|
|
18
18
|
|
|
19
19
|
import { getStakeActivation } from './get-stake-activation/index.js'
|
|
20
|
+
import {
|
|
21
|
+
fetchDelegatedBalances as _fetchDelegatedBalances,
|
|
22
|
+
fetchValidatedDelegation as _fetchValidatedDelegation,
|
|
23
|
+
} from './tx-log/delegation-utils.js'
|
|
20
24
|
|
|
21
25
|
const [SYSTEM_PROGRAM_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID] = [
|
|
22
26
|
SYSTEM_PROGRAM_ID_KEY.toBase58(),
|
|
@@ -62,8 +66,10 @@ export class RpcApi {
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
setTokens(assets) {
|
|
65
|
-
|
|
66
|
-
this.tokens = new Map(
|
|
69
|
+
this.tokensByName = pickBy(assets, (asset) => asset.name !== asset.baseAsset.name)
|
|
70
|
+
this.tokens = new Map(
|
|
71
|
+
Object.values(this.tokensByName).map((token) => [token.mintAddress, token])
|
|
72
|
+
)
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
async rpcCall(method, params = []) {
|
|
@@ -406,6 +412,23 @@ export class RpcApi {
|
|
|
406
412
|
}
|
|
407
413
|
}
|
|
408
414
|
|
|
415
|
+
async fetchValidatedDelegation({ delegatedAddress, expectedDelegate }) {
|
|
416
|
+
return _fetchValidatedDelegation({
|
|
417
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
418
|
+
delegatedAddress,
|
|
419
|
+
expectedDelegate,
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async fetchDelegatedBalances({ delegatedAccounts, address }) {
|
|
424
|
+
return _fetchDelegatedBalances({
|
|
425
|
+
rpcCall: (method, params) => this.rpcCall(method, params),
|
|
426
|
+
delegatedAccounts,
|
|
427
|
+
address,
|
|
428
|
+
assets: this.tokensByName,
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
409
432
|
simulateUnsignedTransaction = async ({ message, transactionMessage }) => {
|
|
410
433
|
const { config, accountAddresses } = getTransactionSimulationParams(
|
|
411
434
|
transactionMessage || message
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createApproveDelegationTx, createRevokeDelegationTx } from '@exodus/solana-lib'
|
|
2
|
+
import assert from 'minimalistic-assert'
|
|
3
|
+
|
|
4
|
+
export const createTokenDelegationFactory = ({ api, assetClientInterface }) => {
|
|
5
|
+
assert(api, 'api is required')
|
|
6
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
7
|
+
|
|
8
|
+
const approveDelegation = async ({
|
|
9
|
+
asset,
|
|
10
|
+
walletAccount,
|
|
11
|
+
delegateAddress,
|
|
12
|
+
amount,
|
|
13
|
+
tokenProgram,
|
|
14
|
+
}) => {
|
|
15
|
+
assert(asset, 'asset is required')
|
|
16
|
+
assert(walletAccount, 'walletAccount is required')
|
|
17
|
+
assert(delegateAddress, 'delegateAddress is required')
|
|
18
|
+
|
|
19
|
+
const baseAssetName = asset.baseAsset.name
|
|
20
|
+
const assetName = asset.name
|
|
21
|
+
const tokenMintAddress = asset.mintAddress
|
|
22
|
+
assert(tokenMintAddress, 'asset must be a token with mintAddress')
|
|
23
|
+
|
|
24
|
+
const ownerAddress = await assetClientInterface.getReceiveAddress({
|
|
25
|
+
assetName: baseAssetName,
|
|
26
|
+
walletAccount,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const recentBlockhash = await api.getRecentBlockHash()
|
|
30
|
+
|
|
31
|
+
const transaction = createApproveDelegationTx({
|
|
32
|
+
ownerAddress,
|
|
33
|
+
tokenMintAddress,
|
|
34
|
+
delegateAddress,
|
|
35
|
+
amount,
|
|
36
|
+
recentBlockhash,
|
|
37
|
+
tokenProgram,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
unsignedTx: {
|
|
42
|
+
txData: { transaction },
|
|
43
|
+
txMeta: { assetName },
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const revokeDelegation = async ({ asset, walletAccount, tokenProgram }) => {
|
|
49
|
+
assert(asset, 'asset is required')
|
|
50
|
+
assert(walletAccount, 'walletAccount is required')
|
|
51
|
+
|
|
52
|
+
const baseAssetName = asset.baseAsset.name
|
|
53
|
+
const assetName = asset.name
|
|
54
|
+
const tokenMintAddress = asset.mintAddress
|
|
55
|
+
assert(tokenMintAddress, 'asset must be a token with mintAddress')
|
|
56
|
+
|
|
57
|
+
const ownerAddress = await assetClientInterface.getReceiveAddress({
|
|
58
|
+
assetName: baseAssetName,
|
|
59
|
+
walletAccount,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const recentBlockhash = await api.getRecentBlockHash()
|
|
63
|
+
|
|
64
|
+
const transaction = createRevokeDelegationTx({
|
|
65
|
+
ownerAddress,
|
|
66
|
+
tokenMintAddress,
|
|
67
|
+
recentBlockhash,
|
|
68
|
+
tokenProgram,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
unsignedTx: {
|
|
73
|
+
txData: { transaction },
|
|
74
|
+
txMeta: { assetName },
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { approveDelegation, revokeDelegation }
|
|
80
|
+
}
|
|
@@ -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
|
}
|