@exodus/solana-api 3.29.2 → 3.29.4
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 +16 -0
- package/package.json +2 -2
- package/src/connection.js +1 -10
- package/src/index.js +1 -2
- package/src/staking/index.js +173 -0
- package/src/token-delegation.js +2 -51
- package/src/ws-api.js +148 -30
- /package/src/{staking-provider-client.js → staking/staking-provider-client.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.3...@exodus/solana-api@3.29.4) (2026-02-13)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @exodus/solana-api
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [3.29.3](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.2...@exodus/solana-api@3.29.3) (2026-02-12)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @exodus/solana-api
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [3.29.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.29.1...@exodus/solana-api@3.29.2) (2026-02-11)
|
|
7
23
|
|
|
8
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.29.
|
|
3
|
+
"version": "3.29.4",
|
|
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",
|
|
@@ -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": "7f0ba4d0c14841809acd9d9365fbe1d38db3835e",
|
|
53
53
|
"bugs": {
|
|
54
54
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
55
55
|
},
|
package/src/connection.js
CHANGED
|
@@ -10,17 +10,8 @@ const PING_INTERVAL = ms('25s')
|
|
|
10
10
|
const debug = debugLogger('exodus:solana-api')
|
|
11
11
|
|
|
12
12
|
export class Connection {
|
|
13
|
-
constructor({
|
|
14
|
-
endpoint,
|
|
15
|
-
address,
|
|
16
|
-
tokensAddresses = [],
|
|
17
|
-
onConnectionReady,
|
|
18
|
-
onConnectionClose,
|
|
19
|
-
onMsg,
|
|
20
|
-
}) {
|
|
13
|
+
constructor({ endpoint, onConnectionReady, onConnectionClose, onMsg }) {
|
|
21
14
|
assert(endpoint, 'endpoint is required')
|
|
22
|
-
this.address = address
|
|
23
|
-
this.tokensAddresses = tokensAddresses
|
|
24
15
|
this.endpoint = endpoint
|
|
25
16
|
this.onConnectionReady = onConnectionReady
|
|
26
17
|
this.onConnectionClose = onConnectionClose
|
package/src/index.js
CHANGED
|
@@ -8,7 +8,6 @@ export { SolanaMonitor } from './tx-log/index.js'
|
|
|
8
8
|
export { SolanaClarityMonitor } from './tx-log/index.js'
|
|
9
9
|
export { SolanaWebsocketMonitor } from './tx-log/index.js'
|
|
10
10
|
export { createAccountState } from './account-state.js'
|
|
11
|
-
export { getStakingInfo } from './staking-utils.js'
|
|
12
11
|
export {
|
|
13
12
|
isSolanaStaking,
|
|
14
13
|
isSolanaUnstaking,
|
|
@@ -18,7 +17,7 @@ export {
|
|
|
18
17
|
export { createAndBroadcastTXFactory } from './tx-send.js'
|
|
19
18
|
export { getBalancesFactory } from './get-balances.js'
|
|
20
19
|
export { getFeeAsyncFactory } from './get-fees.js'
|
|
21
|
-
export {
|
|
20
|
+
export { stakingApiFactory, getStakingInfo } from './staking/index.js'
|
|
22
21
|
export { createTxFactory } from './create-unsigned-tx-for-send.js'
|
|
23
22
|
export { feePayerClientFactory } from './fee-payer.js'
|
|
24
23
|
export { createInitAgentWalletFactory } from './init-agent-wallet.js'
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import chunk from 'lodash/chunk.js'
|
|
2
|
+
|
|
3
|
+
import { getStakingInfo } from '../staking-utils.js'
|
|
4
|
+
import { stakingProviderClientFactory } from './staking-provider-client.js'
|
|
5
|
+
|
|
6
|
+
export { getStakingInfo } from '../staking-utils.js'
|
|
7
|
+
|
|
8
|
+
export const stakingApiFactory = ({ assetName, assetClientInterface, stakingProviderClient }) => {
|
|
9
|
+
const stakingProvider = stakingProviderClient ?? stakingProviderClientFactory()
|
|
10
|
+
|
|
11
|
+
async function sendStake({
|
|
12
|
+
address,
|
|
13
|
+
walletAccount,
|
|
14
|
+
method,
|
|
15
|
+
amount,
|
|
16
|
+
stakeAddresses,
|
|
17
|
+
accounts,
|
|
18
|
+
seed,
|
|
19
|
+
pool,
|
|
20
|
+
}) {
|
|
21
|
+
let error
|
|
22
|
+
let result
|
|
23
|
+
const { [assetName]: asset } = await assetClientInterface.getAssetsForNetwork({
|
|
24
|
+
baseAssetName: assetName,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const { unsignedTx } = await asset.api.createTx({
|
|
29
|
+
asset,
|
|
30
|
+
walletAccount,
|
|
31
|
+
fromAddress: address,
|
|
32
|
+
address,
|
|
33
|
+
amount: amount ?? asset.currency.ZERO,
|
|
34
|
+
method,
|
|
35
|
+
stakeAddresses,
|
|
36
|
+
accounts,
|
|
37
|
+
seed,
|
|
38
|
+
pool,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const sendResult = await asset.api.sendTx({
|
|
42
|
+
asset,
|
|
43
|
+
walletAccount,
|
|
44
|
+
unsignedTx,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
result = { txId: sendResult.txId }
|
|
48
|
+
} catch (err) {
|
|
49
|
+
error = err
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const notifyMethod = {
|
|
54
|
+
delegate: 'notifyStaking',
|
|
55
|
+
withdraw: 'notifyWithdraw',
|
|
56
|
+
}
|
|
57
|
+
if (notifyMethod[method] && (error || result?.txId)) {
|
|
58
|
+
await stakingProvider[notifyMethod[method]]({
|
|
59
|
+
asset: asset.name,
|
|
60
|
+
txId: result?.txId ?? null,
|
|
61
|
+
delegator: address,
|
|
62
|
+
amount: amount?.toBaseString?.({ unit: false }) ?? '0',
|
|
63
|
+
error,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.error('Failed to notify staking service:', e)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { error, result }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function createAndSendStake({ method, address, amount, stakingInfo, walletAccount }) {
|
|
74
|
+
const { [assetName]: asset } = await assetClientInterface.getAssetsForNetwork({
|
|
75
|
+
baseAssetName: assetName,
|
|
76
|
+
})
|
|
77
|
+
const txs = []
|
|
78
|
+
switch (method) {
|
|
79
|
+
case 'delegate': {
|
|
80
|
+
const seed = `exodus:${Date.now()}`
|
|
81
|
+
txs.push({
|
|
82
|
+
method: 'delegate',
|
|
83
|
+
address,
|
|
84
|
+
amount,
|
|
85
|
+
seed,
|
|
86
|
+
pool: stakingInfo.staking.pool,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'undelegate': {
|
|
93
|
+
const addresses = []
|
|
94
|
+
for (const [addr, info] of Object.entries(stakingInfo.accounts)) {
|
|
95
|
+
if (info.state === 'active' || info.state === 'activating') addresses.push(addr)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const N_UNDELEGATE_ADDR = 10
|
|
99
|
+
chunk(addresses, N_UNDELEGATE_ADDR).forEach((stakeAddresses) => {
|
|
100
|
+
txs.push({
|
|
101
|
+
method,
|
|
102
|
+
address,
|
|
103
|
+
amount: asset.currency.ZERO,
|
|
104
|
+
stakeAddresses,
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
break
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case 'withdraw': {
|
|
112
|
+
const accounts = {}
|
|
113
|
+
for (const [addr, info] of Object.entries(stakingInfo.accounts)) {
|
|
114
|
+
if (info.state === 'inactive') accounts[addr] = info
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const N_WITHDRAW_ACCOUNTS = 10
|
|
118
|
+
chunk(Object.entries(accounts), N_WITHDRAW_ACCOUNTS).forEach((entries) => {
|
|
119
|
+
txs.push({
|
|
120
|
+
method,
|
|
121
|
+
address,
|
|
122
|
+
amount: asset.currency.ZERO,
|
|
123
|
+
accounts: Object.fromEntries(entries),
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
if (txs.length === 0) throw new Error('no funds to withdraw')
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
default: {
|
|
131
|
+
throw new Error('Unknown method')
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const txIds = []
|
|
136
|
+
for (const tx of txs) {
|
|
137
|
+
const { error, result } = await sendStake({
|
|
138
|
+
address,
|
|
139
|
+
walletAccount,
|
|
140
|
+
...tx,
|
|
141
|
+
})
|
|
142
|
+
if (error) throw error
|
|
143
|
+
|
|
144
|
+
txIds.push(result.txId)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return txIds
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function delegate({ address, amount, stakingInfo, walletAccount }) {
|
|
151
|
+
return createAndSendStake({ method: 'delegate', address, amount, stakingInfo, walletAccount })
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function undelegate({ address, amount, stakingInfo, walletAccount }) {
|
|
155
|
+
return createAndSendStake({ method: 'undelegate', address, amount, stakingInfo, walletAccount })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function withdraw({ address, amount, stakingInfo, walletAccount }) {
|
|
159
|
+
return createAndSendStake({ method: 'withdraw', address, amount, stakingInfo, walletAccount })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
stake: delegate,
|
|
164
|
+
unstake: undelegate,
|
|
165
|
+
claimUnstaked: withdraw,
|
|
166
|
+
getStakingInfo,
|
|
167
|
+
// Legacy names
|
|
168
|
+
createAndSendStake,
|
|
169
|
+
delegate,
|
|
170
|
+
undelegate,
|
|
171
|
+
withdraw,
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/token-delegation.js
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createApproveDelegationTx,
|
|
3
|
-
createRevokeDelegationTx,
|
|
4
|
-
TOKEN_2022_PROGRAM_ID,
|
|
5
|
-
TOKEN_PROGRAM_ID,
|
|
6
|
-
} from '@exodus/solana-lib'
|
|
1
|
+
import { createApproveDelegationTx, createRevokeDelegationTx } from '@exodus/solana-lib'
|
|
7
2
|
import assert from 'minimalistic-assert'
|
|
8
3
|
|
|
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
4
|
export const createTokenDelegationFactory = ({ api, assetClientInterface }) => {
|
|
26
5
|
assert(api, 'api is required')
|
|
27
6
|
assert(assetClientInterface, 'assetClientInterface is required')
|
|
@@ -97,33 +76,5 @@ export const createTokenDelegationFactory = ({ api, assetClientInterface }) => {
|
|
|
97
76
|
}
|
|
98
77
|
}
|
|
99
78
|
|
|
100
|
-
|
|
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 }
|
|
79
|
+
return { approveDelegation, revokeDelegation }
|
|
129
80
|
}
|
package/src/ws-api.js
CHANGED
|
@@ -14,39 +14,122 @@ const WS_ENDPOINT = 'wss://solana-triton.a.exodus.io/whirligig' // pointing to:
|
|
|
14
14
|
export class WsApi {
|
|
15
15
|
constructor({ rpcUrl, wsUrl, assets }) {
|
|
16
16
|
this.setWsEndpoint(wsUrl)
|
|
17
|
-
this.
|
|
17
|
+
this.connection = null
|
|
18
|
+
this.watchedAddresses = Object.create(null) // address -> { tokensAddresses, onMessage }
|
|
19
|
+
this.#resetSubscriptionState()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Reset subscription maps (used on init and when the WS connection closes). */
|
|
23
|
+
#resetSubscriptionState() {
|
|
18
24
|
this.accountSubscriptions = Object.create(null)
|
|
19
25
|
this.transactionSubscriptions = Object.create(null)
|
|
26
|
+
// subscription id (from RPC response) -> { owner, type: 'account'|'transaction'|'program' }
|
|
27
|
+
this.subscriptionIdToMeta = Object.create(null)
|
|
28
|
+
// request id (our conn.seq) -> { owner, type }, until we receive the subscription id
|
|
29
|
+
this.pendingSubscriptionRequests = Object.create(null)
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
setWsEndpoint(wsUrl) {
|
|
23
33
|
this.wsUrl = wsUrl || WS_ENDPOINT
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Resolve which watched address(es) a message is for; only dispatch to those.
|
|
38
|
+
*/
|
|
39
|
+
#getAddressesForMessage(json) {
|
|
40
|
+
const method = json?.method
|
|
41
|
+
const params = json?.params
|
|
42
|
+
const result = params?.result
|
|
43
|
+
|
|
44
|
+
// Subscription confirmation: store subscription id -> owner and type, do not dispatch
|
|
45
|
+
if (json?.id != null && typeof json?.result === 'number') {
|
|
46
|
+
const pending = this.pendingSubscriptionRequests[json.id]
|
|
47
|
+
if (pending != null) {
|
|
48
|
+
this.subscriptionIdToMeta[json.result] = { owner: pending.owner, type: pending.type }
|
|
49
|
+
delete this.pendingSubscriptionRequests[json.id]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (method === 'accountNotification' || method === 'transactionNotification') {
|
|
56
|
+
const subId = params?.subscription
|
|
57
|
+
if (subId !== null) {
|
|
58
|
+
const owner = this.subscriptionIdToMeta[subId]?.owner
|
|
59
|
+
return owner && this.watchedAddresses[owner] ? [owner] : []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (method === 'programNotification' && result) {
|
|
64
|
+
const owner = this.#getOwnerFromProgramNotificationResult(result)
|
|
65
|
+
return owner && this.watchedAddresses[owner] ? [owner] : []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Ping response, unknown method, etc. – no address to dispatch to
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Extract owner address from programNotification result (Token/Token-2022 account). */
|
|
73
|
+
#getOwnerFromProgramNotificationResult(result) {
|
|
74
|
+
const { value } = result
|
|
75
|
+
if (!value?.account) return null
|
|
76
|
+
const account = value.account
|
|
77
|
+
const parsed = account?.data?.parsed?.info
|
|
78
|
+
if (parsed?.owner) return parsed.owner
|
|
79
|
+
if (Array.isArray(account.data) && account.data[1] === 'base64') {
|
|
80
|
+
try {
|
|
81
|
+
const decoded = Token.decode(Buffer.from(account.data[0], 'base64'))
|
|
82
|
+
return new PublicKey(decoded.owner).toBase58()
|
|
83
|
+
} catch {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#dispatchMessage(json) {
|
|
92
|
+
const addresses = this.#getAddressesForMessage(json)
|
|
93
|
+
for (const addr of addresses) {
|
|
94
|
+
this.watchedAddresses[addr].onMessage(json)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
42
97
|
|
|
43
|
-
|
|
44
|
-
|
|
98
|
+
async #sendAllSubscriptions() {
|
|
99
|
+
for (const address of Object.keys(this.watchedAddresses)) {
|
|
100
|
+
const { tokensAddresses } = this.watchedAddresses[address]
|
|
101
|
+
await this.sendSubscriptions({ address, tokensAddresses })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async watchAddress({ address, tokensAddresses = [], onMessage }) {
|
|
106
|
+
if (this.watchedAddresses[address]) return // already subscribed
|
|
107
|
+
|
|
108
|
+
this.watchedAddresses[address] = { tokensAddresses, onMessage }
|
|
109
|
+
|
|
110
|
+
if (!this.connection) {
|
|
111
|
+
this.connection = new Connection({
|
|
112
|
+
endpoint: this.wsUrl,
|
|
113
|
+
onConnectionReady: () => {
|
|
114
|
+
console.log('SOL WS connected.')
|
|
115
|
+
this.#sendAllSubscriptions().catch((err) => {
|
|
116
|
+
console.error('SOL WS: failed to re-subscribe after connect:', err)
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
onConnectionClose: () => {
|
|
120
|
+
this.#resetSubscriptionState()
|
|
121
|
+
},
|
|
122
|
+
onMsg: (json) => this.#dispatchMessage(json),
|
|
123
|
+
})
|
|
124
|
+
await this.connection.start()
|
|
125
|
+
} else if (this.connection.isOpen) {
|
|
126
|
+
await this.sendSubscriptions({ address, tokensAddresses })
|
|
127
|
+
}
|
|
45
128
|
}
|
|
46
129
|
|
|
47
130
|
async accountSubscribe({ owner, account }) {
|
|
48
131
|
// could be SOL address or token account address
|
|
49
|
-
const conn = this.
|
|
132
|
+
const conn = this.connection
|
|
50
133
|
if (!conn || !conn.isOpen) {
|
|
51
134
|
console.warn('SOL Connection is not open, cannot subscribe to', owner)
|
|
52
135
|
return
|
|
@@ -55,6 +138,8 @@ export class WsApi {
|
|
|
55
138
|
const subscriptions = this.accountSubscriptions[owner] || []
|
|
56
139
|
if (subscriptions?.includes(account)) return // already subscribed
|
|
57
140
|
|
|
141
|
+
const id = ++conn.seq
|
|
142
|
+
this.pendingSubscriptionRequests[id] = { owner, type: 'account' }
|
|
58
143
|
conn.send({
|
|
59
144
|
jsonrpc: '2.0',
|
|
60
145
|
method: 'accountSubscribe',
|
|
@@ -65,7 +150,7 @@ export class WsApi {
|
|
|
65
150
|
commitment: 'confirmed',
|
|
66
151
|
},
|
|
67
152
|
],
|
|
68
|
-
id
|
|
153
|
+
id,
|
|
69
154
|
})
|
|
70
155
|
|
|
71
156
|
this.accountSubscriptions[owner] = [...subscriptions, account]
|
|
@@ -76,7 +161,7 @@ export class WsApi {
|
|
|
76
161
|
accounts = [accounts]
|
|
77
162
|
}
|
|
78
163
|
|
|
79
|
-
const conn = this.
|
|
164
|
+
const conn = this.connection
|
|
80
165
|
if (!conn || !conn.isOpen) {
|
|
81
166
|
console.warn('SOL Connection is not open, cannot subscribe to', owner)
|
|
82
167
|
return
|
|
@@ -87,9 +172,11 @@ export class WsApi {
|
|
|
87
172
|
const difference = accounts.filter((account) => !subscriptions.includes(account))
|
|
88
173
|
if (difference.length === 0) return // already subscribed
|
|
89
174
|
|
|
175
|
+
const id = ++conn.seq
|
|
176
|
+
this.pendingSubscriptionRequests[id] = { owner, type: 'transaction' }
|
|
90
177
|
conn.send({
|
|
91
178
|
jsonrpc: '2.0',
|
|
92
|
-
id
|
|
179
|
+
id,
|
|
93
180
|
method: 'transactionSubscribe',
|
|
94
181
|
params: [
|
|
95
182
|
{
|
|
@@ -111,7 +198,7 @@ export class WsApi {
|
|
|
111
198
|
}
|
|
112
199
|
|
|
113
200
|
async sendSubscriptions({ address, tokensAddresses = [] }) {
|
|
114
|
-
const conn = this.
|
|
201
|
+
const conn = this.connection
|
|
115
202
|
|
|
116
203
|
const addresses = [address, ...tokensAddresses]
|
|
117
204
|
|
|
@@ -129,11 +216,13 @@ export class WsApi {
|
|
|
129
216
|
const token2022ProgramId = TOKEN_2022_PROGRAM_ID.toBase58()
|
|
130
217
|
const tokenAccountDataSize = 165
|
|
131
218
|
|
|
132
|
-
if (conn) {
|
|
219
|
+
if (conn?.isOpen) {
|
|
133
220
|
// SPL Token: fixed 165-byte account size
|
|
221
|
+
const id1 = ++conn.seq
|
|
222
|
+
this.pendingSubscriptionRequests[id1] = { owner: address, type: 'program' }
|
|
134
223
|
conn.send({
|
|
135
224
|
jsonrpc: '2.0',
|
|
136
|
-
id:
|
|
225
|
+
id: id1,
|
|
137
226
|
method: 'programSubscribe',
|
|
138
227
|
params: [
|
|
139
228
|
splTokenProgramId,
|
|
@@ -149,9 +238,11 @@ export class WsApi {
|
|
|
149
238
|
})
|
|
150
239
|
|
|
151
240
|
// Token-2022: no dataSize filter (accounts can have extensions and be larger than 165 bytes)
|
|
241
|
+
const id2 = ++conn.seq
|
|
242
|
+
this.pendingSubscriptionRequests[id2] = { owner: address, type: 'program' }
|
|
152
243
|
conn.send({
|
|
153
244
|
jsonrpc: '2.0',
|
|
154
|
-
id:
|
|
245
|
+
id: id2,
|
|
155
246
|
method: 'programSubscribe',
|
|
156
247
|
params: [
|
|
157
248
|
token2022ProgramId,
|
|
@@ -166,9 +257,36 @@ export class WsApi {
|
|
|
166
257
|
}
|
|
167
258
|
|
|
168
259
|
async unwatchAddress({ address }) {
|
|
169
|
-
if (this.
|
|
170
|
-
|
|
171
|
-
|
|
260
|
+
if (!this.watchedAddresses[address]) return
|
|
261
|
+
|
|
262
|
+
const conn = this.connection
|
|
263
|
+
const unsubMethods = {
|
|
264
|
+
account: 'accountUnsubscribe',
|
|
265
|
+
transaction: 'transactionUnsubscribe',
|
|
266
|
+
program: 'programUnsubscribe',
|
|
267
|
+
}
|
|
268
|
+
for (const [subId, meta] of Object.entries(this.subscriptionIdToMeta)) {
|
|
269
|
+
if (meta.owner !== address) continue
|
|
270
|
+
const method = meta.type && unsubMethods[meta.type]
|
|
271
|
+
if (method && conn?.isOpen) {
|
|
272
|
+
conn.send({
|
|
273
|
+
jsonrpc: '2.0',
|
|
274
|
+
id: ++conn.seq,
|
|
275
|
+
method,
|
|
276
|
+
params: [Number(subId)],
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
delete this.subscriptionIdToMeta[subId]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
delete this.watchedAddresses[address]
|
|
284
|
+
delete this.accountSubscriptions[address]
|
|
285
|
+
delete this.transactionSubscriptions[address]
|
|
286
|
+
|
|
287
|
+
if (Object.keys(this.watchedAddresses).length === 0 && conn) {
|
|
288
|
+
await conn.stop()
|
|
289
|
+
this.connection = null
|
|
172
290
|
}
|
|
173
291
|
}
|
|
174
292
|
|
|
File without changes
|