@exodus/solana-api 3.18.2 → 3.20.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 +30 -0
- package/package.json +2 -3
- package/src/api.js +75 -26
- package/src/create-unsigned-tx-for-send.js +1 -1
- package/src/get-balances.js +52 -21
- package/src/tx-log/solana-monitor.js +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,36 @@
|
|
|
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.20.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.19.0...@exodus/solana-api@3.20.0) (2025-06-11)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat(solana): implement unconfirmedSent/unconfirmedReceived in solana balances (#5802)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.19.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.18.2...@exodus/solana-api@3.19.0) (2025-06-10)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* feat: support SPL batch send monitoring (#5848)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
* fix: SOL amount NaN case (#5713)
|
|
29
|
+
|
|
30
|
+
* fix: SOL check fot NFT rent (#5792)
|
|
31
|
+
|
|
32
|
+
* fix: SOL from/to addresses for token txs (#5751)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
6
36
|
## [3.18.2](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.18.1...@exodus/solana-api@3.18.2) (2025-05-07)
|
|
7
37
|
|
|
8
38
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.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",
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
"@exodus/solana-lib": "^3.11.1",
|
|
34
34
|
"@exodus/solana-meta": "^2.0.2",
|
|
35
35
|
"@exodus/timer": "^1.1.1",
|
|
36
|
-
"bn.js": "^4.11.0",
|
|
37
36
|
"debug": "^4.1.1",
|
|
38
37
|
"delay": "^4.0.1",
|
|
39
38
|
"lodash": "^4.17.11",
|
|
@@ -47,7 +46,7 @@
|
|
|
47
46
|
"@exodus/assets-testing": "^1.0.0",
|
|
48
47
|
"@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
|
|
49
48
|
},
|
|
50
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "a11096f37c71032ca704876059c11b2e55cc592a",
|
|
51
50
|
"bugs": {
|
|
52
51
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
53
52
|
},
|
package/src/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import createApiCJS from '@exodus/asset-json-rpc'
|
|
2
2
|
import { memoizeLruCache } from '@exodus/asset-lib'
|
|
3
|
-
import { memoize } from '@exodus/basic-utils'
|
|
3
|
+
import { memoize, pickBy } from '@exodus/basic-utils'
|
|
4
4
|
import wretch from '@exodus/fetch/wretch'
|
|
5
5
|
import { retry } from '@exodus/simple-retry'
|
|
6
6
|
import {
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
TOKEN_2022_PROGRAM_ID,
|
|
17
17
|
TOKEN_PROGRAM_ID,
|
|
18
18
|
} from '@exodus/solana-lib'
|
|
19
|
-
import BN from 'bn.js'
|
|
20
19
|
import lodash from 'lodash'
|
|
21
20
|
import ms from 'ms'
|
|
22
21
|
import urljoin from 'url-join'
|
|
@@ -32,6 +31,7 @@ const createApi = createApiCJS.default || createApiCJS
|
|
|
32
31
|
const RPC_URL = 'https://solana.a.exodus.io' // https://vip-api.mainnet-beta.solana.com/, https://api.mainnet-beta.solana.com
|
|
33
32
|
const WS_ENDPOINT = 'wss://solana.a.exodus.io/ws' // not standard across all node providers (we're compatible only with Quicknode)
|
|
34
33
|
const FORCE_HTTP = true // use https over ws
|
|
34
|
+
const ZERO = BigInt(0)
|
|
35
35
|
|
|
36
36
|
const errorMessagesToRetry = [
|
|
37
37
|
'Blockhash not found',
|
|
@@ -77,7 +77,7 @@ export class Api {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
setTokens(assets = {}) {
|
|
80
|
-
const solTokens =
|
|
80
|
+
const solTokens = pickBy(assets, (asset) => asset.assetType === this.tokenAssetType)
|
|
81
81
|
this.tokens = new Map(Object.values(solTokens).map((v) => [v.mintAddress, v]))
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -399,18 +399,18 @@ export class Api {
|
|
|
399
399
|
(balance) => balance.accountIndex === postBalance.accountIndex
|
|
400
400
|
)
|
|
401
401
|
|
|
402
|
-
const preAmount =
|
|
403
|
-
const postAmount =
|
|
402
|
+
const preAmount = BigInt(preBalance?.uiTokenAmount?.amount ?? '0')
|
|
403
|
+
const postAmount = BigInt(postBalance?.uiTokenAmount?.amount ?? '0')
|
|
404
404
|
|
|
405
|
-
const amount = postAmount
|
|
405
|
+
const amount = postAmount - preAmount
|
|
406
406
|
|
|
407
|
-
if (!tokenAccount || amount
|
|
407
|
+
if (!tokenAccount || amount === ZERO) return null
|
|
408
408
|
|
|
409
409
|
// This is not perfect as there could be multiple same-token transfers in single
|
|
410
410
|
// transaction, but our wallet only supports one transaction with single txId
|
|
411
411
|
// so we are picking first that matches (correct token + type - send or receive)
|
|
412
412
|
const match = innerInstructions.find((inner) => {
|
|
413
|
-
const targetOwner = amount
|
|
413
|
+
const targetOwner = amount < ZERO ? ownerAddress : null
|
|
414
414
|
return (
|
|
415
415
|
inner.token.mintAddress === tokenAccount.mintAddress && targetOwner === inner.owner
|
|
416
416
|
)
|
|
@@ -433,7 +433,7 @@ export class Api {
|
|
|
433
433
|
owner,
|
|
434
434
|
from,
|
|
435
435
|
to,
|
|
436
|
-
amount: amount
|
|
436
|
+
amount: (amount < ZERO ? -amount : amount).toString(), // inconsistent with the rest, but it can and did overflow
|
|
437
437
|
fee: 0,
|
|
438
438
|
token: tokenAccount,
|
|
439
439
|
data: {
|
|
@@ -563,6 +563,38 @@ export class Api {
|
|
|
563
563
|
})
|
|
564
564
|
.filter((ix) => !!ix)
|
|
565
565
|
|
|
566
|
+
// Collect inner instructions into batch sends
|
|
567
|
+
for (let i = 0; i < innerInstructions.length - 1; i++) {
|
|
568
|
+
const tx = innerInstructions[i]
|
|
569
|
+
|
|
570
|
+
for (let j = i + 1; j < innerInstructions.length; j++) {
|
|
571
|
+
const next = innerInstructions[j]
|
|
572
|
+
if (
|
|
573
|
+
tx.id === next.id &&
|
|
574
|
+
tx.token === next.token &&
|
|
575
|
+
tx.owner === ownerAddress &&
|
|
576
|
+
tx.from === next.from
|
|
577
|
+
) {
|
|
578
|
+
if (!tx.data) {
|
|
579
|
+
tx.data = { sent: [{ address: tx.to, amount: tx.amount }] }
|
|
580
|
+
tx.to = [tx.to]
|
|
581
|
+
tx.fee = 0
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
tx.data.sent.push({
|
|
585
|
+
address: next.to,
|
|
586
|
+
amount: next.amount,
|
|
587
|
+
})
|
|
588
|
+
tx.to.push(next.to)
|
|
589
|
+
|
|
590
|
+
tx.amount += next.amount
|
|
591
|
+
|
|
592
|
+
innerInstructions.splice(j, 1)
|
|
593
|
+
j--
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
566
598
|
// program:type tells us if it's a SOL or Token transfer
|
|
567
599
|
|
|
568
600
|
const stakeTx = lodash.find(instructions, { program: 'system', type: 'createAccountWithSeed' })
|
|
@@ -631,12 +663,26 @@ export class Api {
|
|
|
631
663
|
}
|
|
632
664
|
}
|
|
633
665
|
|
|
666
|
+
const accountIndexes = accountKeys.reduce((acc, key, i) => {
|
|
667
|
+
const hasKnownOwner = tokenAccountsByOwner.some(
|
|
668
|
+
(tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
acc[i] = {
|
|
672
|
+
...key,
|
|
673
|
+
owner: hasKnownOwner ? ownerAddress : null, // not know (like in an outgoing tx)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return acc
|
|
677
|
+
}, Object.create(null)) // { 0: { pubkey, owner }, 1: { ... }, ... }
|
|
678
|
+
|
|
634
679
|
// Parse Token txs
|
|
635
680
|
const tokenTxs = this._parseTokenTransfers({
|
|
636
681
|
instructions,
|
|
637
682
|
tokenAccountsByOwner,
|
|
638
683
|
ownerAddress,
|
|
639
684
|
fee,
|
|
685
|
+
accountIndexes,
|
|
640
686
|
preTokenBalances,
|
|
641
687
|
postTokenBalances,
|
|
642
688
|
})
|
|
@@ -650,20 +696,6 @@ export class Api {
|
|
|
650
696
|
}))
|
|
651
697
|
} else if (preTokenBalances && postTokenBalances) {
|
|
652
698
|
// probably a DEX program is involved (multiple instructions), compute balance changes
|
|
653
|
-
|
|
654
|
-
const accountIndexes = accountKeys.reduce((acc, key, i) => {
|
|
655
|
-
const hasKnownOwner = tokenAccountsByOwner.some(
|
|
656
|
-
(tokenAccount) => tokenAccount.tokenAccountAddress === key.pubkey
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
acc[i] = {
|
|
660
|
-
...key,
|
|
661
|
-
owner: hasKnownOwner ? ownerAddress : null,
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
return acc
|
|
665
|
-
}, Object.create(null))
|
|
666
|
-
|
|
667
699
|
// group by owner and supported token
|
|
668
700
|
const preBalances = preTokenBalances.filter((t) => {
|
|
669
701
|
return (
|
|
@@ -740,6 +772,7 @@ export class Api {
|
|
|
740
772
|
tokenAccountsByOwner,
|
|
741
773
|
ownerAddress,
|
|
742
774
|
fee,
|
|
775
|
+
accountIndexes = {},
|
|
743
776
|
preTokenBalances,
|
|
744
777
|
postTokenBalances,
|
|
745
778
|
}) {
|
|
@@ -769,11 +802,27 @@ export class Api {
|
|
|
769
802
|
delete tokenAccount.balance
|
|
770
803
|
delete tokenAccount.owner
|
|
771
804
|
|
|
805
|
+
// If it's a sending tx we want to have the destination's owner as "to" address
|
|
806
|
+
let to = ownerAddress
|
|
807
|
+
let from = ownerAddress
|
|
808
|
+
if (isSending) {
|
|
809
|
+
to = destination // token account address (trying to get the owner below, we don't always have postTokenBalances...)
|
|
810
|
+
postTokenBalances.forEach((t) => {
|
|
811
|
+
if (accountIndexes[t.accountIndex].pubkey === destination) to = t.owner
|
|
812
|
+
})
|
|
813
|
+
} else {
|
|
814
|
+
// is receiving tx
|
|
815
|
+
from = source // token account address
|
|
816
|
+
preTokenBalances.forEach((t) => {
|
|
817
|
+
if (accountIndexes[t.accountIndex].pubkey === source) from = t.owner
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
|
|
772
821
|
tokenTxs.push({
|
|
773
822
|
owner,
|
|
774
823
|
token: tokenAccount,
|
|
775
|
-
from
|
|
776
|
-
to
|
|
824
|
+
from,
|
|
825
|
+
to,
|
|
777
826
|
amount: Number(amount || tokenAmount?.amount || 0), // supporting types: transfer, transferChecked, transferCheckedWithFee
|
|
778
827
|
fee: isSending ? fee : 0, // in lamports
|
|
779
828
|
})
|
|
@@ -1034,7 +1083,7 @@ export class Api {
|
|
|
1034
1083
|
accounts[addr].canWithdraw = state === 'inactive'
|
|
1035
1084
|
accounts[addr].stake = Number(accounts[addr].stake) || 0 // active staked amount
|
|
1036
1085
|
totalStake += accounts[addr].stake
|
|
1037
|
-
locked +=
|
|
1086
|
+
locked += accounts[addr].state === 'active' ? lamports : 0
|
|
1038
1087
|
activating += accounts[addr].state === 'activating' ? lamports : 0
|
|
1039
1088
|
withdrawable += accounts[addr].canWithdraw ? lamports : 0
|
|
1040
1089
|
pending += accounts[addr].isDeactivating ? lamports : 0
|
|
@@ -210,7 +210,7 @@ export const createUnsignedTxForSend = async ({
|
|
|
210
210
|
|
|
211
211
|
// differentiate between SOL and Solana token
|
|
212
212
|
let isEnoughForRent = false
|
|
213
|
-
if (asset.name === baseAsset.name) {
|
|
213
|
+
if (asset.name === baseAsset.name && !nft) {
|
|
214
214
|
// sending SOL
|
|
215
215
|
isEnoughForRent = amount.gte(rentExemptAmount)
|
|
216
216
|
} else {
|
package/src/get-balances.js
CHANGED
|
@@ -8,7 +8,15 @@ export const getBalancesFactory =
|
|
|
8
8
|
({ asset, accountState, txLog }) => {
|
|
9
9
|
const zero = asset.currency.ZERO
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
balance,
|
|
13
|
+
locked,
|
|
14
|
+
activating,
|
|
15
|
+
withdrawable,
|
|
16
|
+
pending,
|
|
17
|
+
unconfirmedSent,
|
|
18
|
+
unconfirmedReceived,
|
|
19
|
+
} = fixBalances({
|
|
12
20
|
txLog,
|
|
13
21
|
balance: getBalanceFromAccountState({ asset, accountState }),
|
|
14
22
|
locked: accountState.stakingInfo?.locked || zero,
|
|
@@ -16,6 +24,8 @@ export const getBalancesFactory =
|
|
|
16
24
|
activating: accountState.stakingInfo?.activating || zero,
|
|
17
25
|
pending: accountState.stakingInfo?.pending || zero,
|
|
18
26
|
asset,
|
|
27
|
+
unconfirmedSent: zero,
|
|
28
|
+
unconfirmedReceived: zero,
|
|
19
29
|
})
|
|
20
30
|
if (asset.baseAsset.name !== asset.name) {
|
|
21
31
|
return {
|
|
@@ -30,6 +40,7 @@ export const getBalancesFactory =
|
|
|
30
40
|
|
|
31
41
|
const balanceWithoutStaking = balance
|
|
32
42
|
.sub(locked)
|
|
43
|
+
.sub(activating)
|
|
33
44
|
.sub(withdrawable)
|
|
34
45
|
.sub(pending)
|
|
35
46
|
.clampLowerZero()
|
|
@@ -50,6 +61,7 @@ export const getBalancesFactory =
|
|
|
50
61
|
const stakeable = spendable
|
|
51
62
|
|
|
52
63
|
const staked = locked
|
|
64
|
+
const unstaked = withdrawable
|
|
53
65
|
const unstaking = pending
|
|
54
66
|
|
|
55
67
|
const staking = activating || zero
|
|
@@ -63,10 +75,13 @@ export const getBalancesFactory =
|
|
|
63
75
|
spendable,
|
|
64
76
|
stakeable,
|
|
65
77
|
staked,
|
|
78
|
+
unstaked,
|
|
66
79
|
staking,
|
|
67
80
|
unstaking,
|
|
68
81
|
networkReserve,
|
|
69
82
|
walletReserve: zero,
|
|
83
|
+
unconfirmedSent,
|
|
84
|
+
unconfirmedReceived,
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
|
|
@@ -78,40 +93,56 @@ const fixBalances = ({
|
|
|
78
93
|
activating,
|
|
79
94
|
pending,
|
|
80
95
|
asset,
|
|
96
|
+
unconfirmedSent,
|
|
97
|
+
unconfirmedReceived,
|
|
81
98
|
}) => {
|
|
82
99
|
for (const tx of txLog) {
|
|
83
|
-
if (
|
|
100
|
+
if (!tx.pending || tx.error) {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (tx.data.staking) {
|
|
105
|
+
if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
|
|
106
|
+
unconfirmedSent = unconfirmedSent.add(tx.feeAmount)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// staking tx
|
|
110
|
+
switch (tx.data.staking?.method) {
|
|
111
|
+
case 'delegate':
|
|
112
|
+
activating = activating.add(tx.coinAmount.abs())
|
|
113
|
+
break
|
|
114
|
+
case 'withdraw':
|
|
115
|
+
withdrawable = asset.currency.ZERO
|
|
116
|
+
break
|
|
117
|
+
case 'undelegate':
|
|
118
|
+
pending = pending.add(locked).add(activating)
|
|
119
|
+
locked = asset.currency.ZERO
|
|
120
|
+
activating = asset.currency.ZERO
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
} else if (tx.sent) {
|
|
84
124
|
if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
|
|
85
|
-
|
|
125
|
+
unconfirmedSent = unconfirmedSent.add(tx.feeAmount)
|
|
86
126
|
}
|
|
87
127
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
locked = locked.add(tx.coinAmount.abs())
|
|
93
|
-
break
|
|
94
|
-
case 'withdraw':
|
|
95
|
-
withdrawable = asset.currency.ZERO
|
|
96
|
-
break
|
|
97
|
-
case 'undelegate':
|
|
98
|
-
pending = pending.add(locked)
|
|
99
|
-
locked = asset.currency.ZERO
|
|
100
|
-
break
|
|
101
|
-
}
|
|
102
|
-
} else {
|
|
103
|
-
// coinAmount is negative for sent tx
|
|
104
|
-
balance = balance.sub(tx.coinAmount.abs())
|
|
128
|
+
unconfirmedSent = unconfirmedSent.add(tx.coinAmount.abs())
|
|
129
|
+
} else if (tx.received) {
|
|
130
|
+
if (tx.coinAmount.unitType.equals(tx.feeAmount.unitType)) {
|
|
131
|
+
unconfirmedReceived = unconfirmedReceived.sub(tx.feeAmount)
|
|
105
132
|
}
|
|
133
|
+
|
|
134
|
+
unconfirmedReceived = unconfirmedReceived.add(tx.coinAmount.abs())
|
|
106
135
|
}
|
|
107
136
|
}
|
|
108
137
|
|
|
109
138
|
return {
|
|
110
|
-
balance: balance.clampLowerZero(),
|
|
139
|
+
balance: balance.sub(unconfirmedSent).clampLowerZero(),
|
|
111
140
|
locked: locked.clampLowerZero(),
|
|
112
141
|
withdrawable: withdrawable.clampLowerZero(),
|
|
113
142
|
activating: activating.clampLowerZero(),
|
|
114
143
|
pending: pending.clampLowerZero(),
|
|
144
|
+
unconfirmedSent,
|
|
145
|
+
unconfirmedReceived,
|
|
115
146
|
}
|
|
116
147
|
}
|
|
117
148
|
|
|
@@ -217,7 +217,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
217
217
|
if (assetName === 'unknown' || !asset) continue // skip unknown tokens
|
|
218
218
|
const feeAsset = asset.feeAsset
|
|
219
219
|
|
|
220
|
-
const coinAmount = asset.currency.baseUnit(tx.amount)
|
|
220
|
+
const coinAmount = tx.amount ? asset.currency.baseUnit(tx.amount) : asset.currency.ZERO
|
|
221
221
|
|
|
222
222
|
const item = {
|
|
223
223
|
coinName: assetName,
|
|
@@ -245,7 +245,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
245
245
|
if (tx.data?.sent) {
|
|
246
246
|
item.data.sent = tx.data.sent.map((s) => ({
|
|
247
247
|
address: s.address,
|
|
248
|
-
amount:
|
|
248
|
+
amount: asset.currency.baseUnit(s.amount).toDefaultString({ unit: true }),
|
|
249
249
|
}))
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -279,10 +279,9 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
279
279
|
mappedTransactions.push(item)
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
const logItemsByAsset = _.groupBy(mappedTransactions, (item) => item.coinName)
|
|
284
283
|
return {
|
|
285
|
-
logItemsByAsset
|
|
284
|
+
logItemsByAsset,
|
|
286
285
|
hasNewTxs: transactions.length > 0,
|
|
287
286
|
cursorState: { cursor: newCursor },
|
|
288
287
|
}
|
|
@@ -328,11 +327,13 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
328
327
|
: { ...accountState.stakingInfo, staking: this.staking }
|
|
329
328
|
|
|
330
329
|
const stakedBalance = this.asset.currency.baseUnit(staking.locked)
|
|
330
|
+
const activatingBalance = this.asset.currency.baseUnit(staking.activating)
|
|
331
331
|
const withdrawableBalance = this.asset.currency.baseUnit(staking.withdrawable)
|
|
332
332
|
const pendingBalance = this.asset.currency.baseUnit(staking.pending)
|
|
333
333
|
const balance = this.asset.currency
|
|
334
334
|
.baseUnit(solBalance)
|
|
335
335
|
.add(stakedBalance)
|
|
336
|
+
.add(activatingBalance)
|
|
336
337
|
.add(withdrawableBalance)
|
|
337
338
|
.add(pendingBalance)
|
|
338
339
|
|