@exodus/solana-api 3.11.8 → 3.12.0
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 +22 -0
- package/package.json +3 -3
- package/src/account-state.js +1 -0
- package/src/api.js +12 -6
- package/src/get-balances.js +20 -1
- package/src/tx-log/solana-monitor.js +32 -17
- package/src/tx-send.js +4 -16
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,28 @@
|
|
|
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.12.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.9...@exodus/solana-api@3.12.0) (2025-01-02)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: reduce call of Solana getTokenAccountsByOwner (#4762)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.11.9](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.8...@exodus/solana-api@3.11.9) (2024-12-31)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: include rentExemptAmount in balance calculation (#4738)
|
|
23
|
+
|
|
24
|
+
* fix: integration tests (#4720)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## [3.11.8](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.11.6...@exodus/solana-api@3.11.8) (2024-12-10)
|
|
7
29
|
|
|
8
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
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",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@exodus/fetch": "^1.2.0",
|
|
31
31
|
"@exodus/models": "^12.0.1",
|
|
32
32
|
"@exodus/simple-retry": "^0.0.6",
|
|
33
|
-
"@exodus/solana-lib": "^3.9.
|
|
33
|
+
"@exodus/solana-lib": "^3.9.3",
|
|
34
34
|
"@exodus/solana-meta": "^2.0.2",
|
|
35
35
|
"@exodus/timer": "^1.1.1",
|
|
36
36
|
"bn.js": "^4.11.0",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@exodus/assets-testing": "^1.0.0",
|
|
48
48
|
"@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "70de901b0d9b85f1045949400105203c700fc223",
|
|
51
51
|
"bugs": {
|
|
52
52
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
53
53
|
},
|
package/src/account-state.js
CHANGED
package/src/api.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import BN from 'bn.js'
|
|
18
18
|
import lodash from 'lodash'
|
|
19
19
|
import assert from 'minimalistic-assert'
|
|
20
|
+
import ms from 'ms'
|
|
20
21
|
import urljoin from 'url-join'
|
|
21
22
|
import wretch from 'wretch'
|
|
22
23
|
|
|
@@ -46,6 +47,12 @@ export class Api {
|
|
|
46
47
|
const result = await this.rpcCall('getTokenSupply', [mintAddress])
|
|
47
48
|
return result?.value?.amount
|
|
48
49
|
})
|
|
50
|
+
|
|
51
|
+
this.getMinimumBalanceForRentExemption = memoize(
|
|
52
|
+
(accountSize) => this.rpcCall('getMinimumBalanceForRentExemption', [accountSize]),
|
|
53
|
+
(accountSize) => accountSize,
|
|
54
|
+
ms('15m')
|
|
55
|
+
)
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
setServer(rpcUrl) {
|
|
@@ -179,7 +186,10 @@ export class Api {
|
|
|
179
186
|
/**
|
|
180
187
|
* Get transactions from an address
|
|
181
188
|
*/
|
|
182
|
-
async getTransactions(
|
|
189
|
+
async getTransactions(
|
|
190
|
+
address,
|
|
191
|
+
{ cursor, before, limit, includeUnparsed = false, tokenAccounts } = Object.create(null)
|
|
192
|
+
) {
|
|
183
193
|
limit = limit || this.txsLimit
|
|
184
194
|
let transactions = []
|
|
185
195
|
// cursor is a txHash
|
|
@@ -187,7 +197,7 @@ export class Api {
|
|
|
187
197
|
try {
|
|
188
198
|
const until = cursor
|
|
189
199
|
|
|
190
|
-
const tokenAccountsByOwner = await this.getTokenAccountsByOwner(address) // Array
|
|
200
|
+
const tokenAccountsByOwner = tokenAccounts || (await this.getTokenAccountsByOwner(address)) // Array
|
|
191
201
|
const tokenAccountAddresses = tokenAccountsByOwner
|
|
192
202
|
.filter(({ tokenName }) => tokenName !== 'unknown')
|
|
193
203
|
.map(({ tokenAccountAddress }) => tokenAccountAddress)
|
|
@@ -876,10 +886,6 @@ export class Api {
|
|
|
876
886
|
}, 0)
|
|
877
887
|
}
|
|
878
888
|
|
|
879
|
-
async getMinimumBalanceForRentExemption(size) {
|
|
880
|
-
return this.rpcCall('getMinimumBalanceForRentExemption', [size])
|
|
881
|
-
}
|
|
882
|
-
|
|
883
889
|
async getProgramAccounts(programId, config) {
|
|
884
890
|
return this.rpcCall('getProgramAccounts', [programId, config])
|
|
885
891
|
}
|
package/src/get-balances.js
CHANGED
|
@@ -33,7 +33,18 @@ export const getBalancesFactory =
|
|
|
33
33
|
.clampLowerZero()
|
|
34
34
|
|
|
35
35
|
const total = stakingFeatureAvailable ? balance : balanceWithoutStaking
|
|
36
|
-
|
|
36
|
+
|
|
37
|
+
const networkReserve = accountState.rentExemptAmount || zero
|
|
38
|
+
|
|
39
|
+
const accountReserve = asset.accountReserve || zero
|
|
40
|
+
|
|
41
|
+
// there is no wallet reserve when there are no tokens nor staking actions. Just network reserve for the rent exempt amount.
|
|
42
|
+
const walletReserve =
|
|
43
|
+
hasStakedFunds({ locked, withdrawable, pending }) || hasTokensBalance({ accountState })
|
|
44
|
+
? accountReserve.sub(networkReserve).clampLowerZero()
|
|
45
|
+
: zero
|
|
46
|
+
|
|
47
|
+
const spendable = balanceWithoutStaking.sub(walletReserve).sub(networkReserve).clampLowerZero()
|
|
37
48
|
|
|
38
49
|
const staked = locked
|
|
39
50
|
const unstaking = pending
|
|
@@ -47,6 +58,8 @@ export const getBalancesFactory =
|
|
|
47
58
|
spendable,
|
|
48
59
|
staked,
|
|
49
60
|
unstaking,
|
|
61
|
+
networkReserve,
|
|
62
|
+
walletReserve,
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
|
@@ -93,3 +106,9 @@ const getBalanceFromAccountState = ({ asset, accountState }) => {
|
|
|
93
106
|
asset.currency.ZERO
|
|
94
107
|
)
|
|
95
108
|
}
|
|
109
|
+
|
|
110
|
+
const hasStakedFunds = ({ locked, withdrawable, pending }) =>
|
|
111
|
+
[locked, withdrawable, pending].some((amount) => amount.isPositive)
|
|
112
|
+
|
|
113
|
+
const hasTokensBalance = ({ accountState }) =>
|
|
114
|
+
Object.values(accountState?.tokenBalances || {}).some((balance) => balance.isPositive)
|
|
@@ -195,6 +195,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
195
195
|
accountState,
|
|
196
196
|
walletAccount,
|
|
197
197
|
refresh,
|
|
198
|
+
tokenAccounts,
|
|
198
199
|
})
|
|
199
200
|
|
|
200
201
|
const cursorChanged = this.hasNewCursor({ walletAccount, cursorState })
|
|
@@ -210,7 +211,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
210
211
|
}
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
async getHistory({ address, accountState, refresh } =
|
|
214
|
+
async getHistory({ address, accountState, refresh, tokenAccounts } = Object.create(null)) {
|
|
214
215
|
const cursor = refresh ? '' : accountState.cursor
|
|
215
216
|
const baseAsset = this.asset
|
|
216
217
|
|
|
@@ -218,6 +219,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
218
219
|
cursor,
|
|
219
220
|
includeUnparsed: this.includeUnparsed,
|
|
220
221
|
limit: this.txsLimit,
|
|
222
|
+
tokenAccounts,
|
|
221
223
|
})
|
|
222
224
|
|
|
223
225
|
const mappedTransactions = []
|
|
@@ -227,8 +229,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
227
229
|
if (assetName === 'unknown' || !asset) continue // skip unknown tokens
|
|
228
230
|
const feeAsset = asset.feeAsset
|
|
229
231
|
|
|
230
|
-
|
|
231
|
-
const coinAmount = asset.currency.baseUnit(tx.amount).toDefault()
|
|
232
|
+
const coinAmount = asset.currency.baseUnit(tx.amount)
|
|
232
233
|
|
|
233
234
|
const item = {
|
|
234
235
|
coinName: assetName,
|
|
@@ -249,7 +250,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
249
250
|
if (tx.owner === address) {
|
|
250
251
|
// send transaction
|
|
251
252
|
item.to = tx.to
|
|
252
|
-
item.feeAmount = baseAsset.currency.baseUnit(tx.fee)
|
|
253
|
+
item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
|
|
253
254
|
item.feeCoinName = baseAsset.name
|
|
254
255
|
item.coinAmount = item.coinAmount.negate()
|
|
255
256
|
|
|
@@ -259,7 +260,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
259
260
|
}
|
|
260
261
|
} else if (tx.unparsed) {
|
|
261
262
|
if (tx.fee !== 0) {
|
|
262
|
-
item.feeAmount = baseAsset.currency.baseUnit(tx.fee)
|
|
263
|
+
item.feeAmount = baseAsset.currency.baseUnit(tx.fee) // in SOL
|
|
263
264
|
item.feeCoinName = baseAsset.name
|
|
264
265
|
}
|
|
265
266
|
|
|
@@ -294,13 +295,21 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
294
295
|
|
|
295
296
|
async getAccount({ refresh, address, tokenAccounts, accountState, walletAccount }) {
|
|
296
297
|
const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
const accountInfo = await this.api.getAccountInfo(address).catch(() => {})
|
|
299
|
+
const accountSize = accountInfo?.space || 0
|
|
300
|
+
const solBalance = accountInfo?.lamports || 0
|
|
301
|
+
|
|
302
|
+
const rentExemptValue = await this.api.getMinimumBalanceForRentExemption(accountSize)
|
|
303
|
+
const rentExemptAmount = this.asset.currency.baseUnit(rentExemptValue)
|
|
304
|
+
|
|
305
|
+
const splBalances = await this.api.getTokensBalance({
|
|
306
|
+
address,
|
|
307
|
+
filterByTokens: tokens,
|
|
308
|
+
tokenAccounts,
|
|
309
|
+
})
|
|
301
310
|
|
|
302
311
|
const tokenBalances = _.mapValues(splBalances, (balance, name) =>
|
|
303
|
-
this.assets[name].currency.baseUnit(balance)
|
|
312
|
+
this.assets[name].currency.baseUnit(balance)
|
|
304
313
|
)
|
|
305
314
|
|
|
306
315
|
const solBalanceChanged = this.#balanceChanged({
|
|
@@ -328,20 +337,26 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
328
337
|
.add(stakedBalance)
|
|
329
338
|
.add(withdrawableBalance)
|
|
330
339
|
.add(pendingBalance)
|
|
331
|
-
.toDefault()
|
|
332
340
|
|
|
333
341
|
return {
|
|
334
342
|
account: {
|
|
335
343
|
balance,
|
|
336
344
|
tokenBalances,
|
|
345
|
+
rentExemptAmount,
|
|
337
346
|
},
|
|
338
347
|
staking,
|
|
339
348
|
}
|
|
340
349
|
}
|
|
341
350
|
|
|
342
351
|
async updateState({ account, cursorState, walletAccount, staking }) {
|
|
343
|
-
const { balance, tokenBalances } = account
|
|
344
|
-
const newData = {
|
|
352
|
+
const { balance, tokenBalances, rentExemptAmount } = account
|
|
353
|
+
const newData = {
|
|
354
|
+
balance,
|
|
355
|
+
rentExemptAmount,
|
|
356
|
+
tokenBalances,
|
|
357
|
+
stakingInfo: staking,
|
|
358
|
+
...cursorState,
|
|
359
|
+
}
|
|
345
360
|
return this.updateAccountState({ newData, walletAccount })
|
|
346
361
|
}
|
|
347
362
|
|
|
@@ -361,11 +376,11 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
361
376
|
isDelegating: Object.values(stakingInfo.accounts).some(({ state }) =>
|
|
362
377
|
['active', 'activating', 'inactive'].includes(state)
|
|
363
378
|
), // true if at least 1 account is delegating
|
|
364
|
-
locked: this.asset.currency.baseUnit(stakingInfo.locked)
|
|
379
|
+
locked: this.asset.currency.baseUnit(stakingInfo.locked),
|
|
365
380
|
activating: this.asset.currency.baseUnit(stakingInfo.activating),
|
|
366
|
-
withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable)
|
|
367
|
-
pending: this.asset.currency.baseUnit(stakingInfo.pending)
|
|
368
|
-
earned: this.asset.currency.baseUnit(rewards)
|
|
381
|
+
withdrawable: this.asset.currency.baseUnit(stakingInfo.withdrawable),
|
|
382
|
+
pending: this.asset.currency.baseUnit(stakingInfo.pending), // still undelegating (not yet available for withdraw)
|
|
383
|
+
earned: this.asset.currency.baseUnit(rewards),
|
|
369
384
|
accounts: stakingInfo.accounts, // Obj
|
|
370
385
|
}
|
|
371
386
|
}
|
package/src/tx-send.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
TOKEN_2022_PROGRAM_ID,
|
|
6
6
|
TOKEN_PROGRAM_ID,
|
|
7
7
|
} from '@exodus/solana-lib'
|
|
8
|
-
import { transactionToBase58 } from '@exodus/solana-lib/src/tx/common.js'
|
|
9
8
|
import assert from 'minimalistic-assert'
|
|
10
9
|
|
|
11
10
|
export const createAndBroadcastTXFactory =
|
|
@@ -34,6 +33,7 @@ export const createAndBroadcastTXFactory =
|
|
|
34
33
|
expectedMintAddress,
|
|
35
34
|
metadataAddress,
|
|
36
35
|
creators,
|
|
36
|
+
priorityFee,
|
|
37
37
|
// </MagicEden>
|
|
38
38
|
reference,
|
|
39
39
|
memo,
|
|
@@ -129,7 +129,7 @@ export const createAndBroadcastTXFactory =
|
|
|
129
129
|
from,
|
|
130
130
|
to: address,
|
|
131
131
|
amount,
|
|
132
|
-
fee: feeAmount
|
|
132
|
+
fee: feeData.fee, // feeAmount includes the priortyFee
|
|
133
133
|
recentBlockhash,
|
|
134
134
|
feeData,
|
|
135
135
|
reference,
|
|
@@ -139,26 +139,14 @@ export const createAndBroadcastTXFactory =
|
|
|
139
139
|
...magicEdenParams,
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
let { priorityFee } = feeData
|
|
143
|
-
|
|
144
142
|
const transactionForFeeEstimation = prepareForSigning(unsignedTransaction)
|
|
145
143
|
|
|
146
|
-
if (!priorityFee) {
|
|
147
|
-
try {
|
|
148
|
-
priorityFee = await api.getPriorityFee(transactionToBase58(transactionForFeeEstimation))
|
|
149
|
-
} catch (e) {
|
|
150
|
-
console.warn(`Failed to fetch priority fee: ${e.message}`)
|
|
151
|
-
priorityFee = feeData.fallbackPriorityFee
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
144
|
const { unitsConsumed: computeUnits, err } = await api.simulateUnsignedTransaction({
|
|
156
145
|
message: transactionForFeeEstimation.message,
|
|
157
146
|
})
|
|
147
|
+
if (err) throw new Error(JSON.stringify(err))
|
|
158
148
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
unsignedTransaction.txData.priorityFee = priorityFee
|
|
149
|
+
unsignedTransaction.txData.priorityFee = priorityFee ?? 0
|
|
162
150
|
unsignedTransaction.txData.computeUnits = computeUnits * feeData.computeUnitsMultiplier
|
|
163
151
|
|
|
164
152
|
const { txId, rawTx } = await assetClientInterface.signTransaction({
|