@exodus/solana-api 2.5.28 → 2.5.30-patch

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/solana-api",
3
- "version": "2.5.28",
3
+ "version": "2.5.30-patch",
4
4
  "description": "Exodus internal Solana asset API wrapper",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -34,5 +34,5 @@
34
34
  "devDependencies": {
35
35
  "@exodus/assets-testing": "file:../../../__testing__"
36
36
  },
37
- "gitHead": "e46dbfdf19c725e5e7d4aa98a9e0eac913c44ed2"
37
+ "gitHead": "9ec7084bcdfe14f1c6f11df393351885773046a6"
38
38
  }
package/src/api.js CHANGED
@@ -22,8 +22,9 @@ import { Connection } from './connection'
22
22
 
23
23
  // Doc: https://docs.solana.com/apps/jsonrpc-api
24
24
 
25
- const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com, https://solana-api.projectserum.com
26
- const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws'
25
+ const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com
26
+ const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // not standard across all node providers (we're compatible only with Quicknode)
27
+ const FORCE_HTTP = true // use https over ws
27
28
 
28
29
  // Tokens + SOL api support
29
30
  export class Api {
@@ -63,6 +64,7 @@ export class Api {
63
64
  handleReconnect,
64
65
  reconnectDelay,
65
66
  }) {
67
+ if (FORCE_HTTP) return false
66
68
  const conn = new Connection({
67
69
  endpoint: this.wsUrl,
68
70
  address,
@@ -89,7 +91,7 @@ export class Api {
89
91
  if (handleTransfers) return handleTransfers(updates)
90
92
  }
91
93
 
92
- async rpcCall(method, params = [], { address = '', forceHttp = false } = {}) {
94
+ async rpcCall(method, params = [], { address = '', forceHttp = FORCE_HTTP } = {}) {
93
95
  // ws request
94
96
  const connection = this.connections[address] || lodash.sample(Object.values(this.connections)) // pick random connection
95
97
  if (lodash.get(connection, 'isOpen') && !lodash.get(connection, 'shutdown') && !forceHttp) {
@@ -119,7 +121,7 @@ export class Api {
119
121
 
120
122
  async getRecentBlockHash(commitment?) {
121
123
  const result = await this.rpcCall(
122
- 'getRecentBlockhash',
124
+ 'getLatestBlockhash',
123
125
  [{ commitment: commitment || 'finalized', encoding: 'jsonParsed' }],
124
126
  { forceHttp: true }
125
127
  )
@@ -802,7 +804,7 @@ export class Api {
802
804
  */
803
805
  broadcastTransaction = async (signedTx, options) => {
804
806
  console.log('Solana broadcasting TX:', signedTx) // base64
805
- const defaultOptions = { encoding: 'base64', preflightCommitment: 'finalized' }
807
+ const defaultOptions = { encoding: 'base64', preflightCommitment: 'confirmed' }
806
808
 
807
809
  const params = [signedTx, { ...defaultOptions, ...options }]
808
810
  const errorMessagesToRetry = ['Blockhash not found']
@@ -1,21 +1,30 @@
1
+ import { TxSet } from '@exodus/models'
2
+
1
3
  // staking may be a feature that may not be available for a given wallet.
2
4
  // In this case, The wallet should exclude the staking balance from the general balance
3
5
 
4
- export const getBalancesFactory = ({ stakingFeatureAvailable }) => ({ asset, accountState }) => {
6
+ export const getBalancesFactory = ({ stakingFeatureAvailable }) => ({
7
+ asset,
8
+ accountState,
9
+ txLog,
10
+ }) => {
11
+ const zero = asset.currency.ZERO
12
+ const { balance, locked, withdrawable, pending } = fixBalances({
13
+ txLog,
14
+ balance: getBalanceFromAccountState({ asset, accountState }),
15
+ locked: accountState.mem?.locked || zero,
16
+ withdrawable: accountState.mem?.withdrawable || zero,
17
+ pending: accountState.mem?.pending || zero,
18
+ asset,
19
+ })
5
20
  if (asset.baseAsset.name !== asset.name) {
6
- return { balance: accountState?.tokenBalances?.[asset.name], spendableBalance: null }
21
+ return { balance, spendableBalance: balance }
7
22
  }
8
23
 
9
- const zero = asset.currency.ZERO
10
-
11
- const balance = accountState.balance || zero
12
-
13
- const { locked, withdrawable, pending } = accountState.mem || Object.create(null)
14
-
15
24
  const balanceWithoutStaking = balance
16
- .sub(locked || zero)
17
- .sub(withdrawable || zero)
18
- .sub(pending || zero)
25
+ .sub(locked)
26
+ .sub(withdrawable)
27
+ .sub(pending)
19
28
  .clampLowerZero()
20
29
 
21
30
  return {
@@ -23,3 +32,46 @@ export const getBalancesFactory = ({ stakingFeatureAvailable }) => ({ asset, acc
23
32
  spendableBalance: balanceWithoutStaking.sub(asset.accountReserve || zero).clampLowerZero(),
24
33
  }
25
34
  }
35
+
36
+ const fixBalances = ({ txLog = TxSet.EMPTY, balance, locked, withdrawable, pending, asset }) => {
37
+ for (const tx of txLog) {
38
+ if ((tx.sent || tx.data.staking) && tx.pending && !tx.error) {
39
+ if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
40
+ balance = balance.sub(tx.feeAmount)
41
+ }
42
+
43
+ if (!tx.data.staking) {
44
+ // coinAmount is negative for sent tx
45
+ balance = balance.sub(tx.coinAmount.abs())
46
+ } else {
47
+ // staking tx
48
+ switch (tx.data.staking?.method) {
49
+ case 'delegate':
50
+ locked = locked.add(tx.coinAmount.abs())
51
+ break
52
+ case 'withdraw':
53
+ withdrawable = asset.currency.ZERO
54
+ break
55
+ case 'undelegate':
56
+ pending = pending.add(locked)
57
+ locked = asset.currency.ZERO
58
+ break
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return {
64
+ balance: balance.clampLowerZero(),
65
+ locked: locked.clampLowerZero(),
66
+ withdrawable: withdrawable.clampLowerZero(),
67
+ pending: pending.clampLowerZero(),
68
+ }
69
+ }
70
+
71
+ const getBalanceFromAccountState = ({ asset, accountState }) => {
72
+ const isBase = asset.name === asset.baseAsset.name
73
+ return (
74
+ (isBase ? accountState?.balance : accountState?.tokenBalances?.[asset.name]) ||
75
+ asset.currency.ZERO
76
+ )
77
+ }
package/src/index.js CHANGED
@@ -9,6 +9,12 @@ export { default as SolanaFeeMonitor } from './fee-monitor'
9
9
  export { SolanaMonitor } from './tx-log'
10
10
  export { SolanaAccountState } from './account-state'
11
11
  export { getSolStakedFee, getStakingInfo, getUnstakingFee } from './staking-utils'
12
+ export {
13
+ isSolanaStaking,
14
+ isSolanaUnstaking,
15
+ isSolanaWithdrawn,
16
+ isSolanaRewardsActivityTx,
17
+ } from './txs-utils'
12
18
  export { createAndBroadcastTXFactory } from './tx-send'
13
19
  export { getBalancesFactory } from './get-balances'
14
20
 
package/src/tx-send.js CHANGED
@@ -10,7 +10,6 @@ export const createAndBroadcastTXFactory = (api) => async (
10
10
 
11
11
  const {
12
12
  feeAmount,
13
- shouldLog = true,
14
13
  method,
15
14
  stakeAddresses,
16
15
  seed,
@@ -33,21 +32,37 @@ export const createAndBroadcastTXFactory = (api) => async (
33
32
  reference,
34
33
  memo,
35
34
  } = options
36
- let { recentBlockhash } = options
37
35
  const { baseAsset } = asset
38
36
  const from = await assetClientInterface.getReceiveAddress({
39
37
  assetName: baseAsset.name,
40
38
  walletAccount,
41
39
  })
42
- const currentAccountState = await assetClientInterface.getAccountState({
43
- assetName: baseAsset.name,
44
- walletAccount,
45
- })
46
40
 
47
- recentBlockhash = recentBlockhash || (await api.getRecentBlockHash())
41
+ const isToken = asset.assetType === 'SOLANA_TOKEN'
42
+
43
+ // Check if receiver has address active when sending tokens.
44
+ if (isToken) {
45
+ // check address mint is the same
46
+ const targetMint = await api.getAddressMint(address) // null if it's a SOL address
47
+ if (targetMint && targetMint !== asset.mintAddress) {
48
+ const err = new Error('Wrong Destination Wallet')
49
+ err.reason = { mintAddressMismatch: true }
50
+ throw err
51
+ }
52
+ } else {
53
+ // sending SOL
54
+ const addressType = await api.getAddressType(address)
55
+ if (addressType === 'token') {
56
+ const err = new Error('Destination Wallet is a Token address')
57
+ err.reason = { wrongAddressType: true }
58
+ throw err
59
+ }
60
+ }
61
+
62
+ const recentBlockhash = options.recentBlockhash || (await api.getRecentBlockHash())
48
63
 
49
64
  let tokenParams = Object.create(null)
50
- if (asset.assetType === 'SOLANA_TOKEN' || customMintAddress) {
65
+ if (isToken || customMintAddress) {
51
66
  const tokenMintAddress = customMintAddress || asset.mintAddress
52
67
  const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress)
53
68
  const [
@@ -95,7 +110,7 @@ export const createAndBroadcastTXFactory = (api) => async (
95
110
  creators,
96
111
  }
97
112
 
98
- const unsignedTransaction = await createUnsignedTx({
113
+ const unsignedTransaction = createUnsignedTx({
99
114
  asset,
100
115
  from,
101
116
  to: address,
@@ -118,38 +133,36 @@ export const createAndBroadcastTXFactory = (api) => async (
118
133
  await baseAsset.api.broadcastTx(rawTx)
119
134
 
120
135
  const selfSend = from === address
121
- const coinAmount = selfSend ? asset.currency.ZERO : amount.abs().negate()
122
-
123
- if (shouldLog) {
124
- const tx = {
125
- txId,
126
- confirmations: 0,
127
- coinName: assetName,
128
- coinAmount,
129
- feeAmount,
130
- feeCoinName: asset.feeAsset.name,
131
- selfSend,
132
- to: address,
133
- data: {
134
- // note
135
- },
136
- currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
137
- }
138
- await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
136
+ const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
137
+ const coinAmount = isStakingTx
138
+ ? amount.abs()
139
+ : selfSend
140
+ ? asset.currency.ZERO
141
+ : amount.abs().negate()
142
+
143
+ const data = isStakingTx
144
+ ? { staking: { ...stakingParams, stake: coinAmount.toBaseNumber() } }
145
+ : Object.create(null)
146
+
147
+ const tx = {
148
+ txId,
149
+ confirmations: 0,
150
+ coinName: assetName,
151
+ coinAmount,
152
+ feeAmount,
153
+ feeCoinName: asset.feeAsset.name,
154
+ selfSend,
155
+ to: address,
156
+ data,
157
+ currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
139
158
  }
159
+ await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
140
160
 
141
- const changes = Object.create(null)
142
- if (asset.assetType === 'SOLANA_TOKEN') {
143
- Object.assign(changes, {
144
- balance: currentAccountState.balance.sub(feeAmount), // solana balance
145
- tokenBalances: {
146
- ...currentAccountState.tokenBalances,
147
- [assetName]: currentAccountState.tokenBalances[assetName].sub(coinAmount.abs()),
148
- }, // SPL token balance
149
- })
161
+ if (isToken) {
150
162
  // write tx entry in solana for token fee
151
- const tx = {
163
+ const txForFee = {
152
164
  txId,
165
+ confirmations: 0,
153
166
  coinName: baseAsset.name,
154
167
  coinAmount: baseAsset.currency.ZERO,
155
168
  tokens: [assetName],
@@ -162,45 +175,12 @@ export const createAndBroadcastTXFactory = (api) => async (
162
175
  [baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
163
176
  },
164
177
  }
165
- await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
166
- } else if (customMintAddress) {
167
- Object.assign(changes, {
168
- balance: currentAccountState.balance.sub(feeAmount), // solana balance
169
- })
170
- } else if (method) {
171
- // staking: no changes
172
- } else {
173
- // SOL transfer
174
- Object.assign(changes, {
175
- balance: currentAccountState.balance.sub(coinAmount.abs()).sub(feeAmount),
178
+ await assetClientInterface.updateTxLogAndNotify({
179
+ assetName: baseAsset.name,
180
+ walletAccount,
181
+ txs: [txForFee],
176
182
  })
177
183
  }
178
184
 
179
- if (method) {
180
- const stakingData = { ...currentAccountState.mem }
181
- switch (method) {
182
- case 'delegate':
183
- stakingData.isDelegating = true
184
- stakingData.locked = stakingData.locked.add(amount)
185
- break
186
- case 'undelegate':
187
- stakingData.isDelegating = false
188
- stakingData.pending = stakingData.pending.add(stakingData.locked)
189
- stakingData.locked = asset.currency.ZERO
190
- break
191
- case 'withdraw':
192
- stakingData.withdrawable = asset.currency.ZERO
193
- break
194
- }
195
-
196
- Object.assign(changes, { mem: stakingData })
197
- }
198
-
199
- await assetClientInterface.updateAccountState({
200
- assetName: baseAsset.name,
201
- walletAccount,
202
- newData: changes,
203
- })
204
-
205
185
  return { txId }
206
186
  }
@@ -0,0 +1,11 @@
1
+ import { get } from 'lodash'
2
+
3
+ const isSolanaTx = (tx) => tx.coinName === 'solana'
4
+ export const isSolanaStaking = (tx) =>
5
+ isSolanaTx(tx) && ['createAccountWithSeed', 'delegate'].includes(get(tx, 'data.staking.method'))
6
+ export const isSolanaUnstaking = (tx) =>
7
+ isSolanaTx(tx) && get(tx, 'data.staking.method') === 'undelegate'
8
+ export const isSolanaWithdrawn = (tx) =>
9
+ isSolanaTx(tx) && get(tx, 'data.staking.method') === 'withdraw'
10
+ export const isSolanaRewardsActivityTx = (tx) =>
11
+ [isSolanaStaking, isSolanaUnstaking, isSolanaWithdrawn].some((fn) => fn(tx))