@exodus/solana-api 3.11.1 → 3.11.3

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 CHANGED
@@ -3,6 +3,24 @@
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.3](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.2...@exodus/solana-api@3.11.3) (2024-10-23)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * solana-api to use the right tokenAssetType ([#4331](https://github.com/ExodusMovement/assets/issues/4331)) ([d8f0be9](https://github.com/ExodusMovement/assets/commit/d8f0be9be29f286a75a6fdc2c112f004d7816842))
12
+
13
+
14
+
15
+ ## [3.11.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.1...@exodus/solana-api@3.11.2) (2024-10-10)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * SOL doc old references ([#4168](https://github.com/ExodusMovement/assets/issues/4168)) ([73e5516](https://github.com/ExodusMovement/assets/commit/73e5516109f9a3c2012198ef8aa68fc0a5032d5d))
21
+
22
+
23
+
6
24
  ## [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
25
 
8
26
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
1
  # Solana Api · [![npm version](https://img.shields.io/badge/npm-private-blue.svg?style=flat)](https://www.npmjs.com/package/@exodus/solana-api)
2
2
 
3
- - To get all transactions data from an address we gotta call 3 rpcs `getConfirmedSignaturesForAddress2` (get txIds) -> `getConfirmedTransaction` (get tx details) -> `getBlockTime` (get tx timestamp). Pretty annoying and resource-consuming backend-side. (https://github.com/solana-labs/solana/issues/12411)
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.1",
3
+ "version": "3.11.3",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -31,7 +31,7 @@
31
31
  "@exodus/models": "^12.0.1",
32
32
  "@exodus/simple-retry": "^0.0.6",
33
33
  "@exodus/solana-lib": "^3.6.0",
34
- "@exodus/solana-meta": "^2.0.0",
34
+ "@exodus/solana-meta": "^2.0.2",
35
35
  "@exodus/timer": "^1.1.1",
36
36
  "bn.js": "^4.11.0",
37
37
  "debug": "^4.1.1",
@@ -45,9 +45,9 @@
45
45
  },
46
46
  "devDependencies": {
47
47
  "@exodus/assets-testing": "^1.0.0",
48
- "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc2"
48
+ "@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
49
49
  },
50
- "gitHead": "732c0d1ea9b13de6eac6edc49aa8ab866575d8d7",
50
+ "gitHead": "f40ca3367fd84bc4c899c2dfea76004547fb111d",
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
 
@@ -31,17 +33,18 @@ const FORCE_HTTP = true // use https over ws
31
33
 
32
34
  // Tokens + SOL api support
33
35
  export class Api {
34
- constructor({ rpcUrl, wsUrl, assets, txsLimit }) {
36
+ constructor({ rpcUrl, wsUrl, assets, txsLimit, tokenAssetType = 'SOLANA_TOKEN' }) {
35
37
  this.setServer(rpcUrl)
36
38
  this.setWsEndpoint(wsUrl)
37
39
  this.setTokens(assets)
40
+ this.tokenAssetType = tokenAssetType
38
41
  this.tokensToSkip = {}
39
42
  this.txsLimit = txsLimit
40
43
  this.connections = {}
41
- this.getSupply = lodash.memoize(async (mintAddress) => {
44
+ this.getSupply = memoize(async (mintAddress) => {
42
45
  // cached getSupply
43
46
  const result = await this.rpcCall('getTokenSupply', [mintAddress])
44
- return lodash.get(result, 'value.amount')
47
+ return result?.value?.amount
45
48
  })
46
49
  }
47
50
 
@@ -55,7 +58,7 @@ export class Api {
55
58
  }
56
59
 
57
60
  setTokens(assets = {}) {
58
- const solTokens = lodash.pickBy(assets, (asset) => asset.assetType === 'SOLANA_TOKEN')
61
+ const solTokens = lodash.pickBy(assets, (asset) => asset.assetType === this.tokenAssetType)
59
62
  this.tokens = new Map(Object.values(solTokens).map((v) => [v.mintAddress, v]))
60
63
  }
61
64
 
@@ -126,9 +129,9 @@ export class Api {
126
129
  return Number(epoch)
127
130
  }
128
131
 
129
- async getStakeActivation(address) {
130
- const { state } = await this.rpcCall('getStakeActivation', [address])
131
- return state
132
+ async getStakeActivation(stakeAddress) {
133
+ const { status } = await getStakeActivation(this, stakeAddress)
134
+ return status
132
135
  }
133
136
 
134
137
  async getRecentBlockHash(commitment) {
@@ -148,13 +151,6 @@ export class Api {
148
151
  ])
149
152
  }
150
153
 
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
154
  async getPriorityFee(transaction) {
159
155
  // https://docs.helius.dev/solana-rpc-nodes/alpha-priority-fee-api
160
156
  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 = get(feeData, 'fee', this.asset.currency.ZERO)
53
+ const fee = feeData?.fee ?? this.asset.currency.ZERO
57
54
 
58
- const solBalance = get(accountState, 'balance', this.asset.currency.ZERO)
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(
@@ -266,7 +266,11 @@ export class SolanaMonitor extends BaseMonitor {
266
266
  item.data.meta = tx.data.meta
267
267
  }
268
268
 
269
- if (asset.assetType === 'SOLANA_TOKEN' && item.feeAmount && item.feeAmount.isPositive) {
269
+ if (
270
+ asset.assetType === this.api.tokenAssetType &&
271
+ item.feeAmount &&
272
+ item.feeAmount.isPositive
273
+ ) {
270
274
  const feeItem = {
271
275
  ..._.clone(item),
272
276
  coinName: feeAsset.name,
package/src/tx-send.js CHANGED
@@ -44,7 +44,7 @@ export const createAndBroadcastTXFactory =
44
44
  walletAccount,
45
45
  })
46
46
 
47
- const isToken = asset.assetType === 'SOLANA_TOKEN'
47
+ const isToken = asset.assetType === api.tokenAssetType
48
48
 
49
49
  // Check if receiver has address active when sending tokens.
50
50
  if (isToken) {
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(get(tx, 'data.staking.method'))
3
+ isSolanaTx(tx) && ['createAccountWithSeed', 'delegate'].includes(tx?.data?.staking?.method)
8
4
  export const isSolanaUnstaking = (tx) =>
9
- isSolanaTx(tx) && get(tx, 'data.staking.method') === 'undelegate'
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))
@@ -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
- }