@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 +2 -2
- package/src/api.js +7 -5
- package/src/get-balances.js +63 -11
- package/src/index.js +6 -0
- package/src/tx-send.js +54 -74
- package/src/txs-utils.js +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "2.5.
|
|
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": "
|
|
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
|
|
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 =
|
|
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
|
-
'
|
|
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: '
|
|
807
|
+
const defaultOptions = { encoding: 'base64', preflightCommitment: 'confirmed' }
|
|
806
808
|
|
|
807
809
|
const params = [signedTx, { ...defaultOptions, ...options }]
|
|
808
810
|
const errorMessagesToRetry = ['Blockhash not found']
|
package/src/get-balances.js
CHANGED
|
@@ -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 }) => ({
|
|
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
|
|
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
|
|
17
|
-
.sub(withdrawable
|
|
18
|
-
.sub(pending
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
}
|
package/src/txs-utils.js
ADDED
|
@@ -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))
|