@exodus/solana-api 3.14.4 → 3.14.6
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 +20 -0
- package/package.json +2 -2
- package/src/api.js +27 -0
- package/src/create-unsigned-tx-for-send.js +209 -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 +1 -2
- package/src/tx-send.js +85 -195
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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.6](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.14.5...@exodus/solana-api@3.14.6) (2025-04-01)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* fix: SOL staking method param (#5362)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [3.14.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.14.4...@exodus/solana-api@3.14.5) (2025-03-25)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: SOL insufficient funds for rent (#5308)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [3.14.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-api@3.14.3...@exodus/solana-api@3.14.4) (2025-03-20)
|
|
7
27
|
|
|
8
28
|
**Note:** Version bump only for package @exodus/solana-api
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-api",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.6",
|
|
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": "310d0d34a574d1da978f862e42c481de1e57bade",
|
|
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)
|
|
@@ -881,6 +902,12 @@ export class Api {
|
|
|
881
902
|
return owner && owner !== address
|
|
882
903
|
}
|
|
883
904
|
|
|
905
|
+
ataOwnershipChangedCached = memoizeLruCache(
|
|
906
|
+
(...args) => this.ataOwnershipChanged(...args),
|
|
907
|
+
(address, tokenAddress) => `${address}:${tokenAddress}`,
|
|
908
|
+
{ max: 1000 }
|
|
909
|
+
)
|
|
910
|
+
|
|
884
911
|
// Returns account balance of a SPL Token account.
|
|
885
912
|
async getTokenBalance(tokenAddress) {
|
|
886
913
|
const result = await this.rpcCall('getTokenAccountBalance', [tokenAddress])
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
// token related
|
|
22
|
+
tokenStandard,
|
|
23
|
+
customMintAddress,
|
|
24
|
+
// staking
|
|
25
|
+
method,
|
|
26
|
+
stakeAddresses,
|
|
27
|
+
seed,
|
|
28
|
+
pool,
|
|
29
|
+
// <MagicEden>
|
|
30
|
+
initializerAddress,
|
|
31
|
+
initializerDepositTokenAddress,
|
|
32
|
+
takerAmount,
|
|
33
|
+
escrowAddress,
|
|
34
|
+
escrowBump,
|
|
35
|
+
pdaAddress,
|
|
36
|
+
takerAddress,
|
|
37
|
+
expectedTakerAmount,
|
|
38
|
+
expectedMintAddress,
|
|
39
|
+
metadataAddress,
|
|
40
|
+
creators,
|
|
41
|
+
// </MagicEden>
|
|
42
|
+
}) => {
|
|
43
|
+
let tokenParams = Object.create(null)
|
|
44
|
+
const baseAsset = asset.baseAsset
|
|
45
|
+
|
|
46
|
+
if (nft) {
|
|
47
|
+
const [, nftAddress] = nft.id.split(':')
|
|
48
|
+
customMintAddress = nftAddress
|
|
49
|
+
tokenStandard = nft.tokenStandard
|
|
50
|
+
method = tokenStandard === 4 ? 'metaplexTransfer' : undefined
|
|
51
|
+
amount = asset.currency.baseUnit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isToken = asset.assetType === api.tokenAssetType
|
|
55
|
+
|
|
56
|
+
// Check if receiver has address active when sending tokens.
|
|
57
|
+
if (isToken) {
|
|
58
|
+
// check address mint is the same
|
|
59
|
+
const targetMint = await api.getAddressMint(toAddress) // null if it's a SOL address
|
|
60
|
+
if (targetMint && targetMint !== asset.mintAddress) {
|
|
61
|
+
const err = new Error('Wrong Destination Wallet')
|
|
62
|
+
err.mintAddressMismatch = true
|
|
63
|
+
throw err
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// sending SOL
|
|
67
|
+
const addressType = await api.getAddressType(toAddress)
|
|
68
|
+
if (addressType === 'token') {
|
|
69
|
+
const err = new Error('Destination Wallet is a Token address')
|
|
70
|
+
err.wrongAddressType = true
|
|
71
|
+
throw err
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isToken || customMintAddress) {
|
|
76
|
+
const tokenMintAddress = customMintAddress || asset.mintAddress
|
|
77
|
+
const tokenProgramPublicKey =
|
|
78
|
+
(await api.getAddressType(tokenMintAddress)) === 'token-2022'
|
|
79
|
+
? TOKEN_2022_PROGRAM_ID
|
|
80
|
+
: TOKEN_PROGRAM_ID
|
|
81
|
+
|
|
82
|
+
const tokenProgram = tokenProgramPublicKey.toBase58()
|
|
83
|
+
const tokenAddress = findAssociatedTokenAddress(toAddress, tokenMintAddress, tokenProgram)
|
|
84
|
+
|
|
85
|
+
const [destinationAddressType, isAssociatedTokenAccountActive, fromTokenAccountAddresses] =
|
|
86
|
+
await Promise.all([
|
|
87
|
+
api.getAddressType(toAddress),
|
|
88
|
+
api.isAssociatedTokenAccountActive(tokenAddress),
|
|
89
|
+
api.getTokenAccountsByOwner(fromAddress),
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
const changedOwnership = await api.ataOwnershipChangedCached(toAddress, tokenAddress)
|
|
93
|
+
if (changedOwnership) {
|
|
94
|
+
const err = new Error('Destination ATA changed ownership')
|
|
95
|
+
err.ownershipChanged = true
|
|
96
|
+
throw err
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fromTokenAddresses = fromTokenAccountAddresses.filter(
|
|
100
|
+
({ mintAddress }) => mintAddress === tokenMintAddress
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
tokenParams = {
|
|
104
|
+
tokenMintAddress,
|
|
105
|
+
destinationAddressType,
|
|
106
|
+
isAssociatedTokenAccountActive,
|
|
107
|
+
fromTokenAddresses,
|
|
108
|
+
tokenStandard,
|
|
109
|
+
tokenProgram,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stakingParams = {
|
|
114
|
+
method,
|
|
115
|
+
stakeAddresses,
|
|
116
|
+
seed,
|
|
117
|
+
pool,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const recentBlockhash = await api.getRecentBlockHash()
|
|
121
|
+
|
|
122
|
+
const magicEdenParams = {
|
|
123
|
+
method,
|
|
124
|
+
initializerAddress,
|
|
125
|
+
initializerDepositTokenAddress,
|
|
126
|
+
takerAmount,
|
|
127
|
+
escrowAddress,
|
|
128
|
+
escrowBump,
|
|
129
|
+
pdaAddress,
|
|
130
|
+
takerAddress,
|
|
131
|
+
expectedTakerAmount,
|
|
132
|
+
expectedMintAddress,
|
|
133
|
+
metadataAddress,
|
|
134
|
+
creators,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const unsignedTx = createUnsignedTx({
|
|
138
|
+
asset,
|
|
139
|
+
from: fromAddress,
|
|
140
|
+
to: toAddress,
|
|
141
|
+
amount,
|
|
142
|
+
recentBlockhash,
|
|
143
|
+
reference,
|
|
144
|
+
memo,
|
|
145
|
+
...tokenParams,
|
|
146
|
+
...stakingParams,
|
|
147
|
+
...magicEdenParams,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const resolveUnitConsumed = async () => {
|
|
151
|
+
// this avoids unnecessary simulations. Also the simulation fails with InsufficientFundsForRent when sending all.
|
|
152
|
+
if (asset.name === asset.baseAsset.name && amount && !nft && !method) {
|
|
153
|
+
return 150 + CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const transactionForFeeEstimation = prepareForSigning(unsignedTx)
|
|
157
|
+
const { unitsConsumed, err } = await api.simulateUnsignedTransaction({
|
|
158
|
+
message: transactionForFeeEstimation.message,
|
|
159
|
+
})
|
|
160
|
+
if (err) throw new Error(JSON.stringify(err))
|
|
161
|
+
return unitsConsumed + CU_FOR_COMPUTE_BUDGET_INSTRUCTIONS
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const priorityFee = feeData.priorityFee
|
|
165
|
+
let computeUnits
|
|
166
|
+
if (priorityFee) {
|
|
167
|
+
const unitsConsumed = await resolveUnitConsumed()
|
|
168
|
+
computeUnits = unitsConsumed * feeData.computeUnitsMultiplier
|
|
169
|
+
unsignedTx.txData.priorityFee = priorityFee
|
|
170
|
+
unsignedTx.txData.computeUnits = computeUnits
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
unsignedTx.txMeta.stakingParams = stakingParams
|
|
174
|
+
|
|
175
|
+
const fee = feeData.baseFee.add(
|
|
176
|
+
asset.feeAsset.currency
|
|
177
|
+
.baseUnit(unsignedTx.txData.priorityFee ?? 0)
|
|
178
|
+
.mul(unsignedTx.txData.computeUnits ?? 0)
|
|
179
|
+
.div(1_000_000) // micro lamports to lamports
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
// serialization friendlier
|
|
183
|
+
unsignedTx.txMeta.fee = fee.toBaseNumber()
|
|
184
|
+
|
|
185
|
+
const rentExemptValue = await api.getRentExemptionMinAmount(toAddress)
|
|
186
|
+
const rentExemptAmount = baseAsset.currency.baseUnit(rentExemptValue)
|
|
187
|
+
|
|
188
|
+
// differentiate between SOL and Solana token
|
|
189
|
+
let isEnoughForRent = false
|
|
190
|
+
if (asset.name === baseAsset.name) {
|
|
191
|
+
// sending SOL
|
|
192
|
+
isEnoughForRent = amount.gte(rentExemptAmount)
|
|
193
|
+
} else {
|
|
194
|
+
// sending token/nft
|
|
195
|
+
const baseAssetBalance = await api.getBalance(fromAddress)
|
|
196
|
+
isEnoughForRent = baseAsset.currency
|
|
197
|
+
.baseUnit(baseAssetBalance)
|
|
198
|
+
.sub(fee || asset.feeAsset.currency.ZERO)
|
|
199
|
+
.gte(rentExemptAmount)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!isEnoughForRent) {
|
|
203
|
+
const err = new Error('Sending SOL amount is too low to cover the rent exemption fee.')
|
|
204
|
+
err.rentExemptAmount = true
|
|
205
|
+
throw err
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return unsignedTx
|
|
209
|
+
}
|
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.
|
|
@@ -297,10 +297,9 @@ export class SolanaMonitor extends BaseMonitor {
|
|
|
297
297
|
}),
|
|
298
298
|
])
|
|
299
299
|
|
|
300
|
-
const accountSize = accountInfo?.space || 0
|
|
301
300
|
const solBalance = accountInfo?.lamports || 0
|
|
302
301
|
|
|
303
|
-
const rentExemptValue = await this.api.
|
|
302
|
+
const rentExemptValue = await this.api.getRentExemptionMinAmount(address)
|
|
304
303
|
const rentExemptAmount = this.asset.currency.baseUnit(rentExemptValue)
|
|
305
304
|
|
|
306
305
|
const tokenBalances = _.mapValues(splBalances, (balance, name) =>
|
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 }
|