@exodus/solana-api 3.14.3 → 3.14.5
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 +18 -0
- package/package.json +2 -2
- package/src/api.js +47 -17
- package/src/create-unsigned-tx-for-send.js +208 -0
- package/src/get-balances.js +8 -7
- package/src/get-fees.js +27 -0
- package/src/index.js +1 -0
- package/src/tx-log/solana-monitor.js +11 -23
- package/src/tx-send.js +85 -195
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.14.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.14.4...@exodus/solana-api@3.14.5) (2025-03-25)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: SOL insufficient funds for rent (#5308)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.14.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.14.3...@exodus/solana-api@3.14.4) (2025-03-20)
|
|
17
|
+
|
|
18
|
+
**Note:** Version bump only for package @exodus/solana-api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
## [3.14.3](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.14.2...@exodus/solana-api@3.14.3) (2025-03-14)
|
|
7
25
|
|
|
8
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.5",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@exodus/assets-testing": "^1.0.0",
|
|
47
47
|
"@exodus/solana-web3.js": "^1.63.1-exodus.9-rc3"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "9674a3923c06f4c88439e857e88ab5bc67a11891",
|
|
50
50
|
"bugs": {
|
|
51
51
|
"url": "https://github.com/ExodusMovement/assets/issues?q=is%3Aissue+is%3Aopen+label%3Asolana-api"
|
|
52
52
|
},
|
package/src/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import createApiCJS from '@exodus/asset-json-rpc'
|
|
2
|
+
import { memoizeLruCache } from '@exodus/asset-lib'
|
|
2
3
|
import { memoize } from '@exodus/basic-utils'
|
|
3
4
|
import wretch from '@exodus/fetch/wretch'
|
|
4
5
|
import { retry } from '@exodus/simple-retry'
|
|
@@ -58,6 +59,12 @@ export class Api {
|
|
|
58
59
|
(accountSize) => accountSize,
|
|
59
60
|
ms('15m')
|
|
60
61
|
)
|
|
62
|
+
|
|
63
|
+
this.getAccountSize = memoize(
|
|
64
|
+
(address) => this.getAccountInfo(address),
|
|
65
|
+
(address) => address,
|
|
66
|
+
ms('3m')
|
|
67
|
+
)
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
setServer(rpcUrl) {
|
|
@@ -136,6 +143,20 @@ export class Api {
|
|
|
136
143
|
return this.tokens.has(mint)
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
async getRentExemptionMinAmount(address) {
|
|
147
|
+
// minimum amount required for the destination account to be rent-exempt
|
|
148
|
+
const accountInfo = await this.getAccountSize(address).catch(() => {})
|
|
149
|
+
if (accountInfo?.space === 0) {
|
|
150
|
+
// no rent required
|
|
151
|
+
return 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const accountSize = accountInfo?.space || 0
|
|
155
|
+
|
|
156
|
+
// Lamports number
|
|
157
|
+
return this.getMinimumBalanceForRentExemption(accountSize)
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
async getEpochInfo() {
|
|
140
161
|
const { epoch } = await this.rpcCall('getEpochInfo')
|
|
141
162
|
return Number(epoch)
|
|
@@ -841,25 +862,28 @@ export class Api {
|
|
|
841
862
|
: tokenAccounts
|
|
842
863
|
}
|
|
843
864
|
|
|
844
|
-
async
|
|
845
|
-
const accounts =
|
|
865
|
+
async getTokensBalancesAndAccounts({ address, filterByTokens = [] }) {
|
|
866
|
+
const accounts = await this.getTokenAccountsByOwner(address)
|
|
846
867
|
|
|
847
|
-
return
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
acc[tokenName]
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
868
|
+
return {
|
|
869
|
+
balances: accounts.reduce((acc, { tokenName, balance }) => {
|
|
870
|
+
if (
|
|
871
|
+
tokenName === 'unknown' ||
|
|
872
|
+
(filterByTokens.length > 0 && !filterByTokens.includes(tokenName))
|
|
873
|
+
)
|
|
874
|
+
return acc // filter by supported tokens only
|
|
875
|
+
if (acc[tokenName]) {
|
|
876
|
+
acc[tokenName] += Number(balance)
|
|
877
|
+
}
|
|
878
|
+
// e.g { 'serum': 123 }
|
|
879
|
+
else {
|
|
880
|
+
acc[tokenName] = Number(balance)
|
|
881
|
+
} // merge same token account balance
|
|
860
882
|
|
|
861
|
-
|
|
862
|
-
|
|
883
|
+
return acc
|
|
884
|
+
}, {}),
|
|
885
|
+
accounts,
|
|
886
|
+
}
|
|
863
887
|
}
|
|
864
888
|
|
|
865
889
|
async isAssociatedTokenAccountActive(tokenAddress) {
|
|
@@ -878,6 +902,12 @@ export class Api {
|
|
|
878
902
|
return owner && owner !== address
|
|
879
903
|
}
|
|
880
904
|
|
|
905
|
+
ataOwnershipChangedCached = memoizeLruCache(
|
|
906
|
+
(...args) => this.ataOwnershipChanged(...args),
|
|
907
|
+
(address, tokenAddress) => `${address}:${tokenAddress}`,
|
|
908
|
+
{ max: 1000 }
|
|
909
|
+
)
|
|
910
|
+
|
|
881
911
|
// Returns account balance of a SPL Token account.
|
|
882
912
|
async getTokenBalance(tokenAddress) {
|
|
883
913
|
const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createUnsignedTx,
|
|
3
|
+
findAssociatedTokenAddress,
|
|
4
|
+
prepareForSigning,
|
|
5
|
+
TOKEN_2022_PROGRAM_ID,
|
|
6
|
+
TOKEN_PROGRAM_ID,
|
|
7
|
+
} from '@exodus/solana-lib'
|
|
8
|
+
|
|
9
|
+
const CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS = 300
|
|
10
|
+
|
|
11
|
+
export const createUnsignedTxForSend = async ({
|
|
12
|
+
api,
|
|
13
|
+
asset,
|
|
14
|
+
feeData,
|
|
15
|
+
toAddress,
|
|
16
|
+
fromAddress,
|
|
17
|
+
amount,
|
|
18
|
+
reference,
|
|
19
|
+
memo,
|
|
20
|
+
nft,
|
|
21
|
+
/// staking
|
|
22
|
+
stakeAddresses,
|
|
23
|
+
seed,
|
|
24
|
+
pool,
|
|
25
|
+
// <MagicEden>
|
|
26
|
+
initializerAddress,
|
|
27
|
+
initializerDepositTokenAddress,
|
|
28
|
+
takerAmount,
|
|
29
|
+
escrowAddress,
|
|
30
|
+
escrowBump,
|
|
31
|
+
pdaAddress,
|
|
32
|
+
takerAddress,
|
|
33
|
+
expectedTakerAmount,
|
|
34
|
+
expectedMintAddress,
|
|
35
|
+
metadataAddress,
|
|
36
|
+
creators,
|
|
37
|
+
// </MagicEden>
|
|
38
|
+
}) => {
|
|
39
|
+
let method
|
|
40
|
+
let customMintAddress
|
|
41
|
+
let tokenStandard
|
|
42
|
+
let tokenParams = Object.create(null)
|
|
43
|
+
const baseAsset = asset.baseAsset
|
|
44
|
+
|
|
45
|
+
if (nft) {
|
|
46
|
+
const [, nftAddress] = nft.id.split(':')
|
|
47
|
+
customMintAddress = nftAddress
|
|
48
|
+
tokenStandard = nft.tokenStandard
|
|
49
|
+
method = tokenStandard === 4 ? 'metaplexTransfer' : undefined
|
|
50
|
+
amount = asset.currency.baseUnit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isToken = asset.assetType === api.tokenAssetType
|
|
54
|
+
|
|
55
|
+
// Check if receiver has address active when sending tokens.
|
|
56
|
+
if (isToken) {
|
|
57
|
+
// check address mint is the same
|
|
58
|
+
const targetMint = await api.getAddressMint(toAddress) // null if it's a SOL address
|
|
59
|
+
if (targetMint && targetMint !== asset.mintAddress) {
|
|
60
|
+
const err = new Error('Wrong Destination Wallet')
|
|
61
|
+
err.mintAddressMismatch = true
|
|
62
|
+
throw err
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// sending SOL
|
|
66
|
+
const addressType = await api.getAddressType(toAddress)
|
|
67
|
+
if (addressType === 'token') {
|
|
68
|
+
const err = new Error('Destination Wallet is a Token address')
|
|
69
|
+
err.wrongAddressType = true
|
|
70
|
+
throw err
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isToken || customMintAddress) {
|
|
75
|
+
const tokenMintAddress = customMintAddress || asset.mintAddress
|
|
76
|
+
const tokenProgramPublicKey =
|
|
77
|
+
(await api.getAddressType(tokenMintAddress)) === 'token-2022'
|
|
78
|
+
? TOKEN_2022_PROGRAM_ID
|
|
79
|
+
: TOKEN_PROGRAM_ID
|
|
80
|
+
|
|
81
|
+
const tokenProgram = tokenProgramPublicKey.toBase58()
|
|
82
|
+
const tokenAddress = findAssociatedTokenAddress(toAddress, tokenMintAddress, tokenProgram)
|
|
83
|
+
|
|
84
|
+
const [destinationAddressType, isAssociatedTokenAccountActive, fromTokenAccountAddresses] =
|
|
85
|
+
await Promise.all([
|
|
86
|
+
api.getAddressType(toAddress),
|
|
87
|
+
api.isAssociatedTokenAccountActive(tokenAddress),
|
|
88
|
+
api.getTokenAccountsByOwner(fromAddress),
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
const changedOwnership = await api.ataOwnershipChangedCached(toAddress, tokenAddress)
|
|
92
|
+
if (changedOwnership) {
|
|
93
|
+
const err = new Error('Destination ATA changed ownership')
|
|
94
|
+
err.ownershipChanged = true
|
|
95
|
+
throw err
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fromTokenAddresses = fromTokenAccountAddresses.filter(
|
|
99
|
+
({ mintAddress }) => mintAddress === tokenMintAddress
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
tokenParams = {
|
|
103
|
+
tokenMintAddress,
|
|
104
|
+
destinationAddressType,
|
|
105
|
+
isAssociatedTokenAccountActive,
|
|
106
|
+
fromTokenAddresses,
|
|
107
|
+
tokenStandard,
|
|
108
|
+
tokenProgram,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stakingParams = {
|
|
113
|
+
method,
|
|
114
|
+
stakeAddresses,
|
|
115
|
+
seed,
|
|
116
|
+
pool,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const recentBlockhash = await api.getRecentBlockHash()
|
|
120
|
+
|
|
121
|
+
const magicEdenParams = {
|
|
122
|
+
method,
|
|
123
|
+
initializerAddress,
|
|
124
|
+
initializerDepositTokenAddress,
|
|
125
|
+
takerAmount,
|
|
126
|
+
escrowAddress,
|
|
127
|
+
escrowBump,
|
|
128
|
+
pdaAddress,
|
|
129
|
+
takerAddress,
|
|
130
|
+
expectedTakerAmount,
|
|
131
|
+
expectedMintAddress,
|
|
132
|
+
metadataAddress,
|
|
133
|
+
creators,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const unsignedTx = createUnsignedTx({
|
|
137
|
+
asset,
|
|
138
|
+
from: fromAddress,
|
|
139
|
+
to: toAddress,
|
|
140
|
+
amount,
|
|
141
|
+
recentBlockhash,
|
|
142
|
+
reference,
|
|
143
|
+
memo,
|
|
144
|
+
...tokenParams,
|
|
145
|
+
...stakingParams,
|
|
146
|
+
...magicEdenParams,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const resolveUnitConsumed = async () => {
|
|
150
|
+
// this avoids unnecessary simulations. Also the simulation fails with InsufficientFundsForRent when sending all.
|
|
151
|
+
if (asset.name === asset.baseAsset.name && amount && !nft && !method) {
|
|
152
|
+
return 150 + CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const transactionForFeeEstimation = prepareForSigning(unsignedTx)
|
|
156
|
+
const { unitsConsumed, err } = await api.simulateUnsignedTransaction({
|
|
157
|
+
message: transactionForFeeEstimation.message,
|
|
158
|
+
})
|
|
159
|
+
if (err) throw new Error(JSON.stringify(err))
|
|
160
|
+
return unitsConsumed + CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const priorityFee = feeData.priorityFee
|
|
164
|
+
let computeUnits
|
|
165
|
+
if (priorityFee) {
|
|
166
|
+
const unitsConsumed = await resolveUnitConsumed()
|
|
167
|
+
computeUnits = unitsConsumed * feeData.computeUnitsMultiplier
|
|
168
|
+
unsignedTx.txData.priorityFee = priorityFee
|
|
169
|
+
unsignedTx.txData.computeUnits = computeUnits
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
unsignedTx.txMeta.stakingParams = stakingParams
|
|
173
|
+
|
|
174
|
+
const fee = feeData.baseFee.add(
|
|
175
|
+
asset.feeAsset.currency
|
|
176
|
+
.baseUnit(unsignedTx.txData.priorityFee ?? 0)
|
|
177
|
+
.mul(unsignedTx.txData.computeUnits ?? 0)
|
|
178
|
+
.div(1_000_000) // micro lamports to lamports
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// serialization friendlier
|
|
182
|
+
unsignedTx.txMeta.fee = fee.toBaseNumber()
|
|
183
|
+
|
|
184
|
+
const rentExemptValue = await api.getRentExemptionMinAmount(toAddress)
|
|
185
|
+
const rentExemptAmount = baseAsset.currency.baseUnit(rentExemptValue)
|
|
186
|
+
|
|
187
|
+
// differentiate between SOL and Solana token
|
|
188
|
+
let isEnoughForRent = false
|
|
189
|
+
if (asset.name === baseAsset.name) {
|
|
190
|
+
// sending SOL
|
|
191
|
+
isEnoughForRent = amount.gte(rentExemptAmount)
|
|
192
|
+
} else {
|
|
193
|
+
// sending token/nft
|
|
194
|
+
const baseAssetBalance = await api.getBalance(fromAddress)
|
|
195
|
+
isEnoughForRent = baseAsset.currency
|
|
196
|
+
.baseUnit(baseAssetBalance)
|
|
197
|
+
.sub(fee || asset.feeAsset.currency.ZERO)
|
|
198
|
+
.gte(rentExemptAmount)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!isEnoughForRent) {
|
|
202
|
+
const err = new Error('Sending SOL amount is too low to cover the rent exemption fee.')
|
|
203
|
+
err.rentExemptAmount = true
|
|
204
|
+
throw err
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return unsignedTx
|
|
208
|
+
}
|
package/src/get-balances.js
CHANGED
|
@@ -4,7 +4,7 @@ import { TxSet } from '@exodus/models'
|
|
|
4
4
|
// In this case, The wallet should exclude the staking balance from the general balance
|
|
5
5
|
|
|
6
6
|
export const getBalancesFactory =
|
|
7
|
-
({ stakingFeatureAvailable }) =>
|
|
7
|
+
({ stakingFeatureAvailable, allowSendingAll }) =>
|
|
8
8
|
({ asset, accountState, txLog }) => {
|
|
9
9
|
const zero = asset.currency.ZERO
|
|
10
10
|
const { balance, locked, withdrawable, pending } = fixBalances({
|
|
@@ -34,15 +34,16 @@ export const getBalancesFactory =
|
|
|
34
34
|
|
|
35
35
|
const total = stakingFeatureAvailable ? balance : balanceWithoutStaking
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// there is no wallet reserve when there are no tokens nor staking actions. Just network reserve for the rent exempt amount.
|
|
38
|
+
const needsReserve =
|
|
39
|
+
hasStakedFunds({ locked, withdrawable, pending }) || hasTokensBalance({ accountState })
|
|
40
|
+
|
|
41
|
+
const networkReserve =
|
|
42
|
+
allowSendingAll && !needsReserve ? zero : accountState.rentExemptAmount || zero
|
|
38
43
|
|
|
39
44
|
const accountReserve = asset.accountReserve || zero
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
const walletReserve =
|
|
43
|
-
hasStakedFunds({ locked, withdrawable, pending }) || hasTokensBalance({ accountState })
|
|
44
|
-
? accountReserve.sub(networkReserve).clampLowerZero()
|
|
45
|
-
: zero
|
|
46
|
+
const walletReserve = needsReserve ? accountReserve.sub(networkReserve).clampLowerZero() : zero
|
|
46
47
|
|
|
47
48
|
const spendable = balanceWithoutStaking.sub(walletReserve).sub(networkReserve).clampLowerZero()
|
|
48
49
|
|
package/src/get-fees.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import assert from 'minimalistic-assert'
|
|
2
|
+
|
|
3
|
+
import { createUnsignedTxForSend } from './create-unsigned-tx-for-send.js'
|
|
4
|
+
|
|
5
|
+
export const getFeeAsyncFactory = ({ api }) => {
|
|
6
|
+
assert(api, 'api is required')
|
|
7
|
+
return async ({ asset, feeData, unsignedTx: providedUnsignedTx, amount, toAddress, ...rest }) => {
|
|
8
|
+
const unsignedTx =
|
|
9
|
+
providedUnsignedTx ||
|
|
10
|
+
(await createUnsignedTxForSend({
|
|
11
|
+
asset,
|
|
12
|
+
feeData,
|
|
13
|
+
api,
|
|
14
|
+
amount: amount ?? asset.currency.baseUnit(1),
|
|
15
|
+
toAddress: toAddress ?? rest.fromAddress,
|
|
16
|
+
...rest,
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
return { fee: asset.feeAsset.currency.baseUnit(unsignedTx.txMeta.fee), unsignedTx }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getFeeFactory =
|
|
24
|
+
({ asset }) =>
|
|
25
|
+
({ feeData }) => {
|
|
26
|
+
return { fee: feeData.fee }
|
|
27
|
+
}
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export {
|
|
|
15
15
|
} from './txs-utils.js'
|
|
16
16
|
export { createAndBroadcastTXFactory } from './tx-send.js'
|
|
17
17
|
export { getBalancesFactory } from './get-balances.js'
|
|
18
|
+
export { getFeeAsyncFactory } from './get-fees.js'
|
|
18
19
|
export { stakingProviderClientFactory } from './staking-provider-client.js'
|
|
19
20
|
|
|
20
21
|
// These are not the same asset objects as the wallet creates, so they should never be returned to the wallet.
|
|
@@ -143,19 +143,6 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
143
143
|
return true
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
147
|
-
const tokenAccounts = await this.api.getTokenAccountsByOwner(address)
|
|
148
|
-
const { account, staking } = await this.getAccount({
|
|
149
|
-
refresh,
|
|
150
|
-
address,
|
|
151
|
-
tokenAccounts,
|
|
152
|
-
accountState,
|
|
153
|
-
walletAccount,
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
return { account, tokenAccounts, staking }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
146
|
async tick({ walletAccount, refresh }) {
|
|
160
147
|
// Check for new wallet account
|
|
161
148
|
await this.initWalletAccount({ walletAccount })
|
|
@@ -300,21 +287,21 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
300
287
|
}
|
|
301
288
|
}
|
|
302
289
|
|
|
303
|
-
async
|
|
290
|
+
async getAccountsAndBalances({ refresh, address, accountState, walletAccount }) {
|
|
304
291
|
const tokens = Object.keys(this.assets).filter((name) => name !== this.asset.name)
|
|
305
|
-
const accountInfo = await
|
|
306
|
-
|
|
292
|
+
const [accountInfo, { balances: splBalances, accounts: tokenAccounts }] = await Promise.all([
|
|
293
|
+
this.api.getAccountInfo(address).catch(() => {}),
|
|
294
|
+
this.api.getTokensBalancesAndAccounts({
|
|
295
|
+
address,
|
|
296
|
+
filterByTokens: tokens,
|
|
297
|
+
}),
|
|
298
|
+
])
|
|
299
|
+
|
|
307
300
|
const solBalance = accountInfo?.lamports || 0
|
|
308
301
|
|
|
309
|
-
const rentExemptValue = await this.api.
|
|
302
|
+
const rentExemptValue = await this.api.getRentExemptionMinAmount(address)
|
|
310
303
|
const rentExemptAmount = this.asset.currency.baseUnit(rentExemptValue)
|
|
311
304
|
|
|
312
|
-
const splBalances = await this.api.getTokensBalance({
|
|
313
|
-
address,
|
|
314
|
-
filterByTokens: tokens,
|
|
315
|
-
tokenAccounts,
|
|
316
|
-
})
|
|
317
|
-
|
|
318
305
|
const tokenBalances = _.mapValues(splBalances, (balance, name) =>
|
|
319
306
|
this.assets[name].currency.baseUnit(balance)
|
|
320
307
|
)
|
|
@@ -352,6 +339,7 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
352
339
|
rentExemptAmount,
|
|
353
340
|
},
|
|
354
341
|
staking,
|
|
342
|
+
tokenAccounts,
|
|
355
343
|
}
|
|
356
344
|
}
|
|
357
345
|
|
package/src/tx-send.js
CHANGED
|
@@ -1,222 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createUnsignedTx,
|
|
3
|
-
findAssociatedTokenAddress,
|
|
4
|
-
prepareForSigning,
|
|
5
|
-
TOKEN_2022_PROGRAM_ID,
|
|
6
|
-
TOKEN_PROGRAM_ID,
|
|
7
|
-
} from '@exodus/solana-lib'
|
|
8
1
|
import assert from 'minimalistic-assert'
|
|
9
2
|
|
|
3
|
+
import { createUnsignedTxForSend } from './create-unsigned-tx-for-send.js'
|
|
4
|
+
|
|
10
5
|
export const createAndBroadcastTXFactory =
|
|
11
6
|
({ api, assetClientInterface }) =>
|
|
12
|
-
async ({ asset, walletAccount,
|
|
7
|
+
async ({ asset, walletAccount, unsignedTx: predefinedUnsignedTx, ...legacyParams }) => {
|
|
13
8
|
const assetName = asset.name
|
|
14
9
|
assert(assetClientInterface, `assetClientInterface must be supplied in sendTx for ${assetName}`)
|
|
15
10
|
|
|
16
|
-
const
|
|
17
|
-
feeAmount,
|
|
18
|
-
stakeAddresses,
|
|
19
|
-
seed,
|
|
20
|
-
pool,
|
|
21
|
-
nft,
|
|
22
|
-
// <MagicEden>
|
|
23
|
-
initializerAddress,
|
|
24
|
-
initializerDepositTokenAddress,
|
|
25
|
-
takerAmount,
|
|
26
|
-
escrowAddress,
|
|
27
|
-
escrowBump,
|
|
28
|
-
pdaAddress,
|
|
29
|
-
takerAddress,
|
|
30
|
-
expectedTakerAmount,
|
|
31
|
-
expectedMintAddress,
|
|
32
|
-
metadataAddress,
|
|
33
|
-
creators,
|
|
34
|
-
priorityFee,
|
|
35
|
-
// </MagicEden>
|
|
36
|
-
reference,
|
|
37
|
-
memo,
|
|
38
|
-
} = options
|
|
39
|
-
|
|
40
|
-
let { method, customMintAddress, tokenStandard } = options
|
|
41
|
-
|
|
42
|
-
const { baseAsset } = asset
|
|
43
|
-
const from = await assetClientInterface.getReceiveAddress({
|
|
44
|
-
assetName: baseAsset.name,
|
|
45
|
-
walletAccount,
|
|
46
|
-
})
|
|
11
|
+
const baseAsset = asset.baseAsset
|
|
47
12
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
method = tokenStandard === 4 ? 'metaplexTransfer' : undefined
|
|
52
|
-
amount = asset.currency.baseUnit(1)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const isToken = asset.assetType === api.tokenAssetType
|
|
56
|
-
|
|
57
|
-
// Check if receiver has address active when sending tokens.
|
|
58
|
-
if (isToken) {
|
|
59
|
-
// check address mint is the same
|
|
60
|
-
const targetMint = await api.getAddressMint(address) // null if it's a SOL address
|
|
61
|
-
if (targetMint && targetMint !== asset.mintAddress) {
|
|
62
|
-
const err = new Error('Wrong Destination Wallet')
|
|
63
|
-
err.reason = { mintAddressMismatch: true }
|
|
64
|
-
throw err
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
// sending SOL
|
|
68
|
-
const addressType = await api.getAddressType(address)
|
|
69
|
-
if (addressType === 'token') {
|
|
70
|
-
const err = new Error('Destination Wallet is a Token address')
|
|
71
|
-
err.reason = { wrongAddressType: true }
|
|
72
|
-
throw err
|
|
13
|
+
const resolveTxs = async () => {
|
|
14
|
+
if (predefinedUnsignedTx) {
|
|
15
|
+
return predefinedUnsignedTx
|
|
73
16
|
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const recentBlockhash = options.recentBlockhash || (await api.getRecentBlockHash())
|
|
77
|
-
|
|
78
|
-
const feeData = await assetClientInterface.getFeeData({ assetName })
|
|
79
|
-
|
|
80
|
-
let tokenParams = Object.create(null)
|
|
81
|
-
if (isToken || customMintAddress) {
|
|
82
|
-
const tokenMintAddress = customMintAddress || asset.mintAddress
|
|
83
|
-
const tokenProgramPublicKey =
|
|
84
|
-
(await api.getAddressType(tokenMintAddress)) === 'token-2022'
|
|
85
|
-
? TOKEN_2022_PROGRAM_ID
|
|
86
|
-
: TOKEN_PROGRAM_ID
|
|
87
|
-
|
|
88
|
-
const tokenProgram = tokenProgramPublicKey.toBase58()
|
|
89
|
-
const tokenAddress = findAssociatedTokenAddress(address, tokenMintAddress, tokenProgram)
|
|
90
|
-
const [destinationAddressType, isAssociatedTokenAccountActive, fromTokenAccountAddresses] =
|
|
91
|
-
await Promise.all([
|
|
92
|
-
api.getAddressType(address),
|
|
93
|
-
api.isAssociatedTokenAccountActive(tokenAddress),
|
|
94
|
-
api.getTokenAccountsByOwner(from),
|
|
95
|
-
])
|
|
96
|
-
|
|
97
|
-
const changedOwnership = await api.ataOwnershipChanged(address, tokenAddress)
|
|
98
|
-
if (changedOwnership) {
|
|
99
|
-
const err = new Error('Destination ATA changed ownership')
|
|
100
|
-
err.reason = { ownershipChanged: true }
|
|
101
|
-
throw err
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const fromTokenAddresses = fromTokenAccountAddresses.filter(
|
|
105
|
-
({ mintAddress }) => mintAddress === tokenMintAddress
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
tokenParams = {
|
|
109
|
-
tokenMintAddress,
|
|
110
|
-
destinationAddressType,
|
|
111
|
-
isAssociatedTokenAccountActive,
|
|
112
|
-
fromTokenAddresses,
|
|
113
|
-
tokenStandard,
|
|
114
|
-
tokenProgram,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
17
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
18
|
+
const feeData = await assetClientInterface.getFeeData({ assetName })
|
|
19
|
+
const fromAddress = await assetClientInterface.getReceiveAddress({
|
|
20
|
+
assetName: baseAsset.name,
|
|
21
|
+
walletAccount,
|
|
22
|
+
})
|
|
124
23
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
expectedTakerAmount,
|
|
135
|
-
expectedMintAddress,
|
|
136
|
-
metadataAddress,
|
|
137
|
-
creators,
|
|
24
|
+
return createUnsignedTxForSend({
|
|
25
|
+
api,
|
|
26
|
+
asset,
|
|
27
|
+
feeData,
|
|
28
|
+
fromAddress,
|
|
29
|
+
amount: legacyParams.amount,
|
|
30
|
+
toAddress: legacyParams.address,
|
|
31
|
+
...legacyParams.options,
|
|
32
|
+
})
|
|
138
33
|
}
|
|
139
34
|
|
|
140
|
-
const
|
|
141
|
-
asset,
|
|
142
|
-
from,
|
|
143
|
-
to: address,
|
|
144
|
-
amount,
|
|
145
|
-
fee: feeData.fee, // feeAmount includes the priortyFee
|
|
146
|
-
recentBlockhash,
|
|
147
|
-
feeData,
|
|
148
|
-
reference,
|
|
149
|
-
memo,
|
|
150
|
-
...tokenParams,
|
|
151
|
-
...stakingParams,
|
|
152
|
-
...magicEdenParams,
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const transactionForFeeEstimation = prepareForSigning(unsignedTransaction)
|
|
35
|
+
const unsignedTx = await resolveTxs()
|
|
156
36
|
|
|
157
|
-
const
|
|
158
|
-
message: transactionForFeeEstimation.message,
|
|
159
|
-
})
|
|
160
|
-
if (err) throw new Error(JSON.stringify(err))
|
|
161
|
-
|
|
162
|
-
unsignedTransaction.txData.priorityFee = priorityFee ?? 0
|
|
163
|
-
unsignedTransaction.txData.computeUnits = computeUnits * feeData.computeUnitsMultiplier
|
|
164
|
-
|
|
165
|
-
const { txId, rawTx } = await assetClientInterface.signTransaction({
|
|
37
|
+
const signedTx = await assetClientInterface.signTransaction({
|
|
166
38
|
assetName: baseAsset.name,
|
|
167
|
-
unsignedTx
|
|
39
|
+
unsignedTx,
|
|
168
40
|
walletAccount,
|
|
169
41
|
})
|
|
170
42
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const selfSend = from === address
|
|
174
|
-
const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
|
|
175
|
-
const coinAmount = isStakingTx
|
|
176
|
-
? amount.abs()
|
|
177
|
-
: selfSend
|
|
178
|
-
? asset.currency.ZERO
|
|
179
|
-
: amount.abs().negate()
|
|
180
|
-
|
|
181
|
-
const data = isStakingTx
|
|
182
|
-
? { staking: { ...stakingParams, stake: coinAmount.toBaseNumber() } }
|
|
183
|
-
: Object.create(null)
|
|
184
|
-
const tx = {
|
|
185
|
-
txId,
|
|
186
|
-
confirmations: 0,
|
|
187
|
-
coinName: assetName,
|
|
188
|
-
coinAmount,
|
|
189
|
-
feeAmount,
|
|
190
|
-
feeCoinName: asset.feeAsset.name,
|
|
191
|
-
selfSend,
|
|
192
|
-
to: address,
|
|
193
|
-
data,
|
|
194
|
-
currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
|
|
195
|
-
}
|
|
196
|
-
await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
|
|
43
|
+
const txId = signedTx.txId
|
|
197
44
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
45
|
+
const isToken = asset.assetType === api.tokenAssetType
|
|
46
|
+
|
|
47
|
+
await baseAsset.api.broadcastTx(signedTx.rawTx)
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// collecting data from unsignedTx, it should be sufficient
|
|
51
|
+
const method = unsignedTx.txData.method
|
|
52
|
+
const fromAddress = unsignedTx.txData.from
|
|
53
|
+
const toAddress = unsignedTx.txData.to
|
|
54
|
+
const selfSend = fromAddress === toAddress
|
|
55
|
+
const amount = asset.currency.baseUnit(unsignedTx.txData.amount)
|
|
56
|
+
const feeAmount = asset.feeAsset.currency.baseUnit(unsignedTx.txMeta.fee)
|
|
57
|
+
|
|
58
|
+
const isStakingTx = ['delegate', 'undelegate', 'withdraw'].includes(method)
|
|
59
|
+
const coinAmount =
|
|
60
|
+
(isStakingTx ? amount?.abs() : selfSend ? asset.currency.ZERO : amount?.abs().negate()) ||
|
|
61
|
+
asset.currency.ZERO
|
|
62
|
+
|
|
63
|
+
let data
|
|
64
|
+
if (isStakingTx) {
|
|
65
|
+
data = { ...unsignedTx?.txMeta.stakingParams, stake: coinAmount.toBaseNumber() }
|
|
66
|
+
} else {
|
|
67
|
+
data = Object.create(null)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const tx = {
|
|
201
71
|
txId,
|
|
202
72
|
confirmations: 0,
|
|
203
|
-
coinName:
|
|
204
|
-
coinAmount
|
|
205
|
-
tokens: [assetName],
|
|
73
|
+
coinName: assetName,
|
|
74
|
+
coinAmount,
|
|
206
75
|
feeAmount,
|
|
207
|
-
feeCoinName:
|
|
208
|
-
to: address,
|
|
76
|
+
feeCoinName: asset.feeAsset.name,
|
|
209
77
|
selfSend,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
},
|
|
78
|
+
to: toAddress,
|
|
79
|
+
data,
|
|
80
|
+
currencies: { [assetName]: asset.currency, [asset.feeAsset.name]: asset.feeAsset.currency },
|
|
214
81
|
}
|
|
215
|
-
await assetClientInterface.updateTxLogAndNotify({
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
82
|
+
await assetClientInterface.updateTxLogAndNotify({ assetName, walletAccount, txs: [tx] })
|
|
83
|
+
|
|
84
|
+
if (isToken) {
|
|
85
|
+
// write tx entry in solana for token fee
|
|
86
|
+
const txForFee = {
|
|
87
|
+
txId,
|
|
88
|
+
confirmations: 0,
|
|
89
|
+
coinName: baseAsset.name,
|
|
90
|
+
coinAmount: baseAsset.currency.ZERO,
|
|
91
|
+
tokens: [assetName],
|
|
92
|
+
feeAmount,
|
|
93
|
+
feeCoinName: baseAsset.feeAsset.name,
|
|
94
|
+
to: toAddress,
|
|
95
|
+
selfSend,
|
|
96
|
+
currencies: {
|
|
97
|
+
[baseAsset.name]: baseAsset.currency,
|
|
98
|
+
[baseAsset.feeAsset.name]: baseAsset.feeAsset.currency,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
await assetClientInterface.updateTxLogAndNotify({
|
|
102
|
+
assetName: baseAsset.name,
|
|
103
|
+
walletAccount,
|
|
104
|
+
txs: [txForFee],
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.log('error writing SOL txLog', err)
|
|
109
|
+
return { txId, txLogError: true }
|
|
220
110
|
}
|
|
221
111
|
|
|
222
112
|
return { txId }
|