@exodus/solana-api 3.11.1 → 3.11.2
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 +9 -0
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/api.js +7 -12
- package/src/get-stake-activation/delegation.js +153 -0
- package/src/get-stake-activation/index.js +63 -0
- package/src/index.js +0 -1
- package/src/tx-log/solana-auto-withdraw-monitor.js +2 -5
- package/src/txs-utils.js +3 -8
- package/src/fee-monitor.js +0 -19
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
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.11.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.1...@exodus/solana-api@3.11.2) (2024-10-10)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* SOL doc old references ([#4168](https://github.com/ExodusMovement/assets/issues/4168)) ([73e5516](https://github.com/ExodusMovement/assets/commit/73e5516109f9a3c2012198ef8aa68fc0a5032d5d))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## [3.11.1](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.0...@exodus/solana-api@3.11.1) (2024-09-11)
|
|
7
16
|
|
|
8
17
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# Solana Api · [](https://www.npmjs.com/package/@exodus/solana-api)
|
|
2
2
|
|
|
3
|
-
- To get all transactions data from an address we gotta call 3 rpcs `
|
|
3
|
+
- To get all transactions data from an address we gotta call 3 rpcs `getSignaturesForAddress` (get txIds) -> `getTransaction` (get tx details) -> `getBlockTime` (get tx timestamp). Pretty annoying and resource-consuming backend-side. (https://github.com/solana-labs/solana/issues/12411)
|
|
4
4
|
- calling `getBlockTime` might results in an error if the slot/block requested is too old (https://github.com/solana-labs/solana/issues/12413), looks like some Solana validators can choose to not keep all the ledger blocks (fix in progress by solana team).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.2",
|
|
4
4
|
"description": "Exodus internal Solana asset API wrapper",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@exodus/assets-testing": "^1.0.0",
|
|
48
48
|
"@exodus/solana-web3.js": "^1.63.1-exodus.9-rc2"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "45abf17ba22cc6a66b112118ee2157dc55bcbde0",
|
|
51
51
|
"bugs": {
|
|
52
52
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
53
53
|
},
|
package/src/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import createApiCJS from '@exodus/asset-json-rpc'
|
|
2
|
+
import { memoize } from '@exodus/basic-utils'
|
|
2
3
|
import { retry } from '@exodus/simple-retry'
|
|
3
4
|
import {
|
|
4
5
|
buildRawTransaction,
|
|
@@ -20,6 +21,7 @@ import urljoin from 'url-join'
|
|
|
20
21
|
import wretch from 'wretch'
|
|
21
22
|
|
|
22
23
|
import { Connection } from './connection.js'
|
|
24
|
+
import { getStakeActivation } from './get-stake-activation/index.js'
|
|
23
25
|
|
|
24
26
|
const createApi = createApiCJS.default || createApiCJS
|
|
25
27
|
|
|
@@ -38,10 +40,10 @@ export class Api {
|
|
|
38
40
|
this.tokensToSkip = {}
|
|
39
41
|
this.txsLimit = txsLimit
|
|
40
42
|
this.connections = {}
|
|
41
|
-
this.getSupply =
|
|
43
|
+
this.getSupply = memoize(async (mintAddress) => {
|
|
42
44
|
// cached getSupply
|
|
43
45
|
const result = await this.rpcCall('getTokenSupply', [mintAddress])
|
|
44
|
-
return
|
|
46
|
+
return result?.value?.amount
|
|
45
47
|
})
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -126,9 +128,9 @@ export class Api {
|
|
|
126
128
|
return Number(epoch)
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
async getStakeActivation(
|
|
130
|
-
const {
|
|
131
|
-
return
|
|
131
|
+
async getStakeActivation(stakeAddress) {
|
|
132
|
+
const { status } = await getStakeActivation(this, stakeAddress)
|
|
133
|
+
return status
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
async getRecentBlockHash(commitment) {
|
|
@@ -148,13 +150,6 @@ export class Api {
|
|
|
148
150
|
])
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
async getFee() {
|
|
152
|
-
const result = await this.rpcCall('getRecentBlockhash', [
|
|
153
|
-
{ commitment: 'finalized', encoding: 'jsonParsed' },
|
|
154
|
-
])
|
|
155
|
-
return lodash.get(result, 'value.feeCalculator.lamportsPerSignature')
|
|
156
|
-
}
|
|
157
|
-
|
|
158
153
|
async getPriorityFee(transaction) {
|
|
159
154
|
// https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api
|
|
160
155
|
const result = await this.rpcCall('getPriorityFeeEstimate', [
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const WARMUP_COOLDOWN_RATE = 0.09
|
|
2
|
+
|
|
3
|
+
function getStakeHistoryEntry(epoch, stakeHistory) {
|
|
4
|
+
for (const entry of stakeHistory) {
|
|
5
|
+
if (entry.epoch === epoch) {
|
|
6
|
+
return entry
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getStakeAndActivating(
|
|
14
|
+
delegation, // Delegation
|
|
15
|
+
targetEpoch, // number
|
|
16
|
+
stakeHistory // StakeHistoryEntry[]
|
|
17
|
+
) {
|
|
18
|
+
if (delegation.activationEpoch === delegation.deactivationEpoch) {
|
|
19
|
+
// activated but instantly deactivated; no stake at all regardless of target_epoch
|
|
20
|
+
return {
|
|
21
|
+
effective: 0,
|
|
22
|
+
activating: 0,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (targetEpoch === delegation.activationEpoch) {
|
|
27
|
+
// all is activating
|
|
28
|
+
return {
|
|
29
|
+
effective: 0,
|
|
30
|
+
activating: delegation.stake,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (targetEpoch < delegation.activationEpoch) {
|
|
35
|
+
// not yet enabled
|
|
36
|
+
return {
|
|
37
|
+
effective: 0,
|
|
38
|
+
activating: 0,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let currentEpoch = delegation.activationEpoch
|
|
43
|
+
let entry = getStakeHistoryEntry(currentEpoch, stakeHistory)
|
|
44
|
+
if (entry !== null) {
|
|
45
|
+
// target_epoch > self.activation_epoch
|
|
46
|
+
|
|
47
|
+
// loop from my activation epoch until the target epoch summing up my entitlement
|
|
48
|
+
// current effective stake is updated using its previous epoch's cluster stake
|
|
49
|
+
let currentEffectiveStake = 0
|
|
50
|
+
while (entry !== null) {
|
|
51
|
+
currentEpoch++
|
|
52
|
+
const remaining = delegation.stake - currentEffectiveStake
|
|
53
|
+
const weight = Number(remaining) / Number(entry.activating)
|
|
54
|
+
const newlyEffectiveClusterStake = Number(entry.effective) * WARMUP_COOLDOWN_RATE
|
|
55
|
+
const newlyEffectiveStake = Math.max(1, Math.round(weight * newlyEffectiveClusterStake))
|
|
56
|
+
|
|
57
|
+
currentEffectiveStake += newlyEffectiveStake
|
|
58
|
+
if (currentEffectiveStake >= delegation.stake) {
|
|
59
|
+
currentEffectiveStake = delegation.stake
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (currentEpoch >= targetEpoch || currentEpoch >= delegation.deactivationEpoch) {
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
entry = getStakeHistoryEntry(currentEpoch, stakeHistory)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
effective: currentEffectiveStake,
|
|
72
|
+
activating: delegation.stake - currentEffectiveStake,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// no history or I've dropped out of history, so assume fully effective
|
|
77
|
+
return {
|
|
78
|
+
effective: delegation.stake,
|
|
79
|
+
activating: 0,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getStakeActivatingAndDeactivating(delegation, targetEpoch, stakeHistory) {
|
|
84
|
+
const { effective, activating } = getStakeAndActivating(delegation, targetEpoch, stakeHistory)
|
|
85
|
+
|
|
86
|
+
// then de-activate some portion if necessary
|
|
87
|
+
if (targetEpoch < delegation.deactivationEpoch) {
|
|
88
|
+
return {
|
|
89
|
+
effective,
|
|
90
|
+
activating,
|
|
91
|
+
deactivating: 0,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (targetEpoch === delegation.deactivationEpoch) {
|
|
96
|
+
// can only deactivate what's activated
|
|
97
|
+
return {
|
|
98
|
+
effective,
|
|
99
|
+
activating: 0,
|
|
100
|
+
deactivating: effective,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let currentEpoch = delegation.deactivationEpoch
|
|
105
|
+
let entry = getStakeHistoryEntry(currentEpoch, stakeHistory)
|
|
106
|
+
if (entry !== null) {
|
|
107
|
+
// target_epoch > self.activation_epoch
|
|
108
|
+
// loop from my deactivation epoch until the target epoch
|
|
109
|
+
// current effective stake is updated using its previous epoch's cluster stake
|
|
110
|
+
let currentEffectiveStake = effective
|
|
111
|
+
while (entry !== null) {
|
|
112
|
+
currentEpoch++
|
|
113
|
+
// if there is no deactivating stake at prev epoch, we should have been
|
|
114
|
+
// fully undelegated at this moment
|
|
115
|
+
if (entry.deactivating === 0) {
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// I'm trying to get to zero, how much of the deactivation in stake
|
|
120
|
+
// this account is entitled to take
|
|
121
|
+
const weight = Number(currentEffectiveStake) / Number(entry.deactivating)
|
|
122
|
+
|
|
123
|
+
// portion of newly not-effective cluster stake I'm entitled to at current epoch
|
|
124
|
+
const newlyNotEffectiveClusterStake = Number(entry.effective) * WARMUP_COOLDOWN_RATE
|
|
125
|
+
const newlyNotEffectiveStake = Math.max(1, Math.round(weight * newlyNotEffectiveClusterStake))
|
|
126
|
+
|
|
127
|
+
currentEffectiveStake -= newlyNotEffectiveStake
|
|
128
|
+
if (currentEffectiveStake <= 0) {
|
|
129
|
+
currentEffectiveStake = 0
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (currentEpoch >= targetEpoch) {
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
entry = getStakeHistoryEntry(currentEpoch, stakeHistory)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// deactivating stake should equal to all of currently remaining effective stake
|
|
141
|
+
return {
|
|
142
|
+
effective: currentEffectiveStake,
|
|
143
|
+
deactivating: currentEffectiveStake,
|
|
144
|
+
activating: 0,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
effective: 0,
|
|
150
|
+
activating: 0,
|
|
151
|
+
deactivating: 0,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getStakeActivatingAndDeactivating } from './delegation.js'
|
|
2
|
+
|
|
3
|
+
const SYSVAR_STAKE_HISTORY_ADDRESS = 'SysvarStakeHistory1111111111111111111111111'
|
|
4
|
+
|
|
5
|
+
// Extracted from https://github.com/anza-xyz/solana-rpc-client-extensions/blob/main/js/src/rpc.ts
|
|
6
|
+
|
|
7
|
+
export async function getStakeActivation(api, stakeAddress) {
|
|
8
|
+
const [epoch, stakeAccount, stakeHistory] = await Promise.all([
|
|
9
|
+
api.getEpochInfo(),
|
|
10
|
+
(async () => {
|
|
11
|
+
const stakeAccount = await api.getAccountInfo(stakeAddress)
|
|
12
|
+
if (stakeAccount.data.discriminant === 0) {
|
|
13
|
+
throw new Error('data.discriminant is 0')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return stakeAccount
|
|
17
|
+
})(),
|
|
18
|
+
(async () => {
|
|
19
|
+
return api.getAccountInfo(SYSVAR_STAKE_HISTORY_ADDRESS)
|
|
20
|
+
})(),
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
const rentExemptReserve = stakeAccount.data.parsed.info.meta.rentExemptReserve
|
|
24
|
+
if (stakeAccount.data.parsed.discriminant === 1) {
|
|
25
|
+
return {
|
|
26
|
+
status: 'inactive',
|
|
27
|
+
active: 0,
|
|
28
|
+
inactive: stakeAccount.lamports - rentExemptReserve,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// THE HARD PART
|
|
33
|
+
const { effective, activating, deactivating } = stakeAccount.data.parsed.info.stake
|
|
34
|
+
? getStakeActivatingAndDeactivating(
|
|
35
|
+
stakeAccount.data.parsed.info.stake.delegation,
|
|
36
|
+
epoch,
|
|
37
|
+
stakeHistory.data.parsed.info
|
|
38
|
+
)
|
|
39
|
+
: {
|
|
40
|
+
effective: 0,
|
|
41
|
+
activating: 0,
|
|
42
|
+
deactivating: 0,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let status
|
|
46
|
+
if (deactivating > 0) {
|
|
47
|
+
status = 'deactivating'
|
|
48
|
+
} else if (activating > 0) {
|
|
49
|
+
status = 'activating'
|
|
50
|
+
} else if (effective > 0) {
|
|
51
|
+
status = 'active'
|
|
52
|
+
} else {
|
|
53
|
+
status = 'inactive'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inactive = stakeAccount.lamports - effective - rentExemptReserve
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
status,
|
|
60
|
+
active: effective,
|
|
61
|
+
inactive,
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import assetsList from '@exodus/solana-meta'
|
|
|
4
4
|
|
|
5
5
|
import { Api } from './api.js'
|
|
6
6
|
|
|
7
|
-
export { default as SolanaFeeMonitor } from './fee-monitor.js'
|
|
8
7
|
export { SolanaMonitor, SolanaAutoWithdrawMonitor } from './tx-log/index.js'
|
|
9
8
|
export { createAccountState } from './account-state.js'
|
|
10
9
|
export { getSolStakedFee, getStakingInfo, getUnstakingFee } from './staking-utils.js'
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { Timer } from '@exodus/timer'
|
|
2
|
-
import lodash from 'lodash'
|
|
3
2
|
import assert from 'minimalistic-assert'
|
|
4
3
|
import ms from 'ms'
|
|
5
4
|
|
|
6
|
-
const { get } = lodash
|
|
7
|
-
|
|
8
5
|
const INTERVAL = ms('30s')
|
|
9
6
|
|
|
10
7
|
export class SolanaAutoWithdrawMonitor {
|
|
@@ -53,9 +50,9 @@ export class SolanaAutoWithdrawMonitor {
|
|
|
53
50
|
async tryWithdraw({ accountState, walletAccount }) {
|
|
54
51
|
const stakingInfo = accountState.stakingInfo
|
|
55
52
|
const feeData = await this.aci.getFeeData({ assetName: this.assetName })
|
|
56
|
-
const fee =
|
|
53
|
+
const fee = feeData?.fee ?? this.asset.currency.ZERO
|
|
57
54
|
|
|
58
|
-
const solBalance =
|
|
55
|
+
const solBalance = accountState?.balance ?? this.asset.currency.ZERO
|
|
59
56
|
if (solBalance.lt(fee) || stakingInfo.withdrawable.isZero) return []
|
|
60
57
|
|
|
61
58
|
const promises = await this.createAndSendStake(
|
package/src/txs-utils.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import lodash from 'lodash'
|
|
2
|
-
|
|
3
|
-
const { get } = lodash
|
|
4
|
-
|
|
5
1
|
const isSolanaTx = (tx) => tx.coinName === 'solana'
|
|
6
2
|
export const isSolanaStaking = (tx) =>
|
|
7
|
-
isSolanaTx(tx) && ['createAccountWithSeed', 'delegate'].includes(
|
|
3
|
+
isSolanaTx(tx) && ['createAccountWithSeed', 'delegate'].includes(tx?.data?.staking?.method)
|
|
8
4
|
export const isSolanaUnstaking = (tx) =>
|
|
9
|
-
isSolanaTx(tx) &&
|
|
10
|
-
export const isSolanaWithdrawn = (tx) =>
|
|
11
|
-
isSolanaTx(tx) && get(tx, 'data.staking.method') === 'withdraw'
|
|
5
|
+
isSolanaTx(tx) && tx?.data?.staking?.method === 'undelegate'
|
|
6
|
+
export const isSolanaWithdrawn = (tx) => isSolanaTx(tx) && tx?.data?.staking?.method === 'withdraw'
|
|
12
7
|
export const isSolanaRewardsActivityTx = (tx) =>
|
|
13
8
|
[isSolanaStaking, isSolanaUnstaking, isSolanaWithdrawn].some((fn) => fn(tx))
|
package/src/fee-monitor.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { FeeMonitor } from '@exodus/asset-lib'
|
|
2
|
-
|
|
3
|
-
export default class SolanaFeeMonitor extends FeeMonitor {
|
|
4
|
-
#api
|
|
5
|
-
|
|
6
|
-
constructor({ updateFee, interval = '1m', assetName = 'solana', api }) {
|
|
7
|
-
super({ updateFee, interval, assetName })
|
|
8
|
-
this.#api = api
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async fetchFee() {
|
|
12
|
-
const fee = await this.#api.getFee()
|
|
13
|
-
if (fee === undefined) throw new Error('Failed to fetch fee')
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
fee: `${fee} Lamports`,
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|