@exodus/solana-plugin 1.36.4 → 1.37.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 +20 -0
- package/package.json +3 -3
- package/src/create-asset.js +14 -3
- package/src/move-funds.js +159 -0
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
|
+
## [1.37.0](https://github.com/ExodusMovement/assets/compare/@exodus/solana-plugin@1.36.5...@exodus/solana-plugin@1.37.0) (2026-04-27)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
* feat: add MoveFunds API for Solana (#7870)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [1.36.5](https://github.com/ExodusMovement/assets/compare/@exodus/solana-plugin@1.36.4...@exodus/solana-plugin@1.36.5) (2026-04-16)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
* fix: solana devnet monitorType (#7816)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
## [1.36.4](https://github.com/ExodusMovement/assets/compare/@exodus/solana-plugin@1.36.3...@exodus/solana-plugin@1.36.4) (2026-04-04)
|
|
7
27
|
|
|
8
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/solana-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Solana plugin for Exodus SDK powered wallets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@exodus/i18n-dummy": "^1.0.0",
|
|
29
29
|
"@exodus/send-validation-model": "^1.0.0",
|
|
30
30
|
"@exodus/solana-api": "^3.30.9",
|
|
31
|
-
"@exodus/solana-lib": "^3.
|
|
31
|
+
"@exodus/solana-lib": "^3.24.0",
|
|
32
32
|
"@exodus/solana-meta": "^2.9.0",
|
|
33
33
|
"@exodus/web3-solana-utils": "^2.9.0",
|
|
34
34
|
"minimalistic-assert": "^1.0.1",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"type": "git",
|
|
45
45
|
"url": "git+https://github.com/ExodusMovement/assets.git"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "a6dde437af961c72c62f64eff53207ccfaeacc95"
|
|
48
48
|
}
|
package/src/create-asset.js
CHANGED
|
@@ -32,6 +32,7 @@ import ms from 'ms'
|
|
|
32
32
|
|
|
33
33
|
import { createHistoryMonitorFactory, securityChecks } from './create-asset-utils.js'
|
|
34
34
|
import { createGetBalanceForAddress } from './get-balance-for-address.js'
|
|
35
|
+
import { moveFundsFactory } from './move-funds.js'
|
|
35
36
|
import sendValidationsFactory from './send-validations.js'
|
|
36
37
|
import { createWeb3API } from './web3/index.js'
|
|
37
38
|
|
|
@@ -44,7 +45,7 @@ export const createSolanaAssetFactory =
|
|
|
44
45
|
({
|
|
45
46
|
assetClientInterface,
|
|
46
47
|
config: {
|
|
47
|
-
monitorType
|
|
48
|
+
monitorType,
|
|
48
49
|
stakingFeatureAvailable = true,
|
|
49
50
|
includeUnparsed = false,
|
|
50
51
|
allowSendingAll = true,
|
|
@@ -62,6 +63,7 @@ export const createSolanaAssetFactory =
|
|
|
62
63
|
} = {},
|
|
63
64
|
overrideCallback = ({ asset }) => asset,
|
|
64
65
|
} = {}) => {
|
|
66
|
+
const resolvedMonitorType = isTestnet ? 'rpc' : monitorType ?? 'ws-clarity'
|
|
65
67
|
const assets = connectAssetsList(assetList)
|
|
66
68
|
const { name: baseAssetName } = assetList.find((asset) => asset.baseAssetName === asset.name)
|
|
67
69
|
const base = assets[baseAssetName]
|
|
@@ -69,7 +71,7 @@ export const createSolanaAssetFactory =
|
|
|
69
71
|
const rpcApi = new Api({ rpcUrl, assets })
|
|
70
72
|
const clarityApi = new ClarityApi({ assets })
|
|
71
73
|
const wsApi = new WsApi({ assets })
|
|
72
|
-
const defaultApi = ['ws-clarity', 'clarity'].includes(
|
|
74
|
+
const defaultApi = ['ws-clarity', 'clarity'].includes(resolvedMonitorType) ? clarityApi : rpcApi
|
|
73
75
|
|
|
74
76
|
const smallTxAmount = base.currency.defaultUnit('0.0001')
|
|
75
77
|
const accountReserve = base.currency.defaultUnit(
|
|
@@ -119,6 +121,13 @@ export const createSolanaAssetFactory =
|
|
|
119
121
|
assetClientInterface,
|
|
120
122
|
})
|
|
121
123
|
|
|
124
|
+
const moveFunds = moveFundsFactory({
|
|
125
|
+
baseAssetName,
|
|
126
|
+
solanaApi: rpcApi,
|
|
127
|
+
assetClientInterface,
|
|
128
|
+
createTx,
|
|
129
|
+
})
|
|
130
|
+
|
|
122
131
|
const stakingApi = stakingApiFactory({
|
|
123
132
|
assetName: baseAssetName,
|
|
124
133
|
assetClientInterface,
|
|
@@ -162,6 +171,7 @@ export const createSolanaAssetFactory =
|
|
|
162
171
|
family: ASSET_FAMILY.SOLANA,
|
|
163
172
|
feeMonitor: false,
|
|
164
173
|
feesApi: true,
|
|
174
|
+
moveFunds: true,
|
|
165
175
|
nfts: true,
|
|
166
176
|
staking: {},
|
|
167
177
|
isTestnet,
|
|
@@ -209,7 +219,7 @@ export const createSolanaAssetFactory =
|
|
|
209
219
|
})
|
|
210
220
|
|
|
211
221
|
const createHistoryMonitor = createHistoryMonitorFactory({
|
|
212
|
-
monitorType,
|
|
222
|
+
monitorType: resolvedMonitorType,
|
|
213
223
|
assetClientInterface,
|
|
214
224
|
interval: monitorInterval,
|
|
215
225
|
shouldUpdateBalanceBeforeHistory,
|
|
@@ -247,6 +257,7 @@ export const createSolanaAssetFactory =
|
|
|
247
257
|
.filter((asset) => asset.name !== base.name)
|
|
248
258
|
.map(createToken),
|
|
249
259
|
hasFeature: (feature) => !!features[feature], // @deprecated use api.features instead
|
|
260
|
+
moveFunds,
|
|
250
261
|
privateKeyEncodingDefinition: { encoding: 'base58', data: 'priv|pub' },
|
|
251
262
|
securityChecks,
|
|
252
263
|
sendTx,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAddressFromMpcKey,
|
|
3
|
+
getAddressFromPrivateKey,
|
|
4
|
+
getPrivateKeyFromSecretKey,
|
|
5
|
+
isMpcKey,
|
|
6
|
+
isValidAddress,
|
|
7
|
+
signUnsignedTx,
|
|
8
|
+
signUnsignedTxWithMpcKey,
|
|
9
|
+
} from '@exodus/solana-lib'
|
|
10
|
+
import assert from 'minimalistic-assert'
|
|
11
|
+
|
|
12
|
+
const importers = {
|
|
13
|
+
seed: {
|
|
14
|
+
getAddress: (secretKey) => {
|
|
15
|
+
const privateKey = getPrivateKeyFromSecretKey(secretKey)
|
|
16
|
+
return getAddressFromPrivateKey(privateKey)
|
|
17
|
+
},
|
|
18
|
+
sign: (unsignedTx, secretKey) => {
|
|
19
|
+
const privateKey = getPrivateKeyFromSecretKey(secretKey)
|
|
20
|
+
return signUnsignedTx(unsignedTx, privateKey)
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
mpc: {
|
|
24
|
+
getAddress: getAddressFromMpcKey,
|
|
25
|
+
sign: signUnsignedTxWithMpcKey,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const getImporter = async (secretKey) => {
|
|
30
|
+
const isMpc = await isMpcKey(secretKey)
|
|
31
|
+
return isMpc ? importers.mpc : importers.seed
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Sentinel walletAccount used when calling baseAsset.api.createTx from the move-funds context.
|
|
35
|
+
// createTx requires a truthy walletAccount; for move-funds (external key import) there is no
|
|
36
|
+
// real wallet account, so we pass this value. The only wallet-account-specific call inside
|
|
37
|
+
// createTx (getAccountState for token delegation) is not relevant for move-funds.
|
|
38
|
+
const MOVE_FUNDS_WALLET_ACCOUNT = 'exodus:move-funds'
|
|
39
|
+
|
|
40
|
+
export const moveFundsFactory = ({ baseAssetName, solanaApi, assetClientInterface, createTx }) => {
|
|
41
|
+
assert(solanaApi, 'solanaApi is required')
|
|
42
|
+
assert(assetClientInterface, 'assetClientInterface is required')
|
|
43
|
+
assert(createTx, 'createTx is required')
|
|
44
|
+
|
|
45
|
+
async function prepareSendFundsTx({ assetName, input, toAddress, MoveFundsError }) {
|
|
46
|
+
assert(assetName, 'assetName is required')
|
|
47
|
+
assert(input, 'input is required')
|
|
48
|
+
assert(toAddress && isValidAddress(toAddress), 'valid toAddress is required')
|
|
49
|
+
assert(MoveFundsError, 'MoveFundsError is required')
|
|
50
|
+
|
|
51
|
+
// MPC keys are base58-encoded 64-byte values where the first 32 bytes are a scalar
|
|
52
|
+
// (not a seed). Check before the regular base58 path since both share the same encoding.
|
|
53
|
+
const secretKey = input.trim()
|
|
54
|
+
|
|
55
|
+
let fromAddress
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const importer = await getImporter(secretKey)
|
|
59
|
+
fromAddress = await importer.getAddress(secretKey)
|
|
60
|
+
} catch {
|
|
61
|
+
throw new MoveFundsError('private-key-invalid')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (fromAddress === toAddress) {
|
|
65
|
+
throw new MoveFundsError('private-key-own-key', { fromAddress })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const assets = await assetClientInterface.getAssetsForNetwork({ baseAssetName })
|
|
69
|
+
const asset = assets[assetName]
|
|
70
|
+
assert(asset, 'asset is required')
|
|
71
|
+
|
|
72
|
+
const baseAsset = asset.baseAsset
|
|
73
|
+
const isToken = asset.name !== baseAssetName
|
|
74
|
+
|
|
75
|
+
const rawBalance = await solanaApi.getBalance(fromAddress)
|
|
76
|
+
const balance = baseAsset.currency.baseUnit(rawBalance)
|
|
77
|
+
|
|
78
|
+
if (!balance || balance.isZero) {
|
|
79
|
+
throw new MoveFundsError('balance-zero', { fromAddress })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let unsignedTx
|
|
83
|
+
let fee
|
|
84
|
+
|
|
85
|
+
if (isToken) {
|
|
86
|
+
const { balances } = await solanaApi.getTokensBalancesAndAccounts({ address: fromAddress })
|
|
87
|
+
const amount = asset.currency.baseUnit(balances?.[assetName] ?? 0)
|
|
88
|
+
|
|
89
|
+
// Token transfer: fee is paid in SOL, full token balance is sent.
|
|
90
|
+
try {
|
|
91
|
+
;({ unsignedTx, fee } = await createTx({
|
|
92
|
+
asset,
|
|
93
|
+
walletAccount: MOVE_FUNDS_WALLET_ACCOUNT,
|
|
94
|
+
fromAddress,
|
|
95
|
+
toAddress,
|
|
96
|
+
amount,
|
|
97
|
+
useFeePayer: false,
|
|
98
|
+
}))
|
|
99
|
+
} catch {
|
|
100
|
+
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (balance.lt(fee)) {
|
|
104
|
+
throw new MoveFundsError('token-fee-insufficient', { fromAddress })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { fromAddress, toAddress, amount, fee, secretKey, unsignedTx }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// SOL transfer: estimate fee via a self-send (stable estimation, avoids destination
|
|
111
|
+
// rent-exemption check for new accounts), then create the real tx with balance - fee.
|
|
112
|
+
try {
|
|
113
|
+
;({ fee } = await createTx({
|
|
114
|
+
asset,
|
|
115
|
+
walletAccount: MOVE_FUNDS_WALLET_ACCOUNT,
|
|
116
|
+
fromAddress,
|
|
117
|
+
toAddress: fromAddress,
|
|
118
|
+
amount: asset.currency.baseUnit(1),
|
|
119
|
+
useFeePayer: false,
|
|
120
|
+
}))
|
|
121
|
+
} catch {
|
|
122
|
+
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const amount = balance.sub(fee)
|
|
126
|
+
if (amount.isNegative || amount.isZero) {
|
|
127
|
+
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
;({ unsignedTx, fee } = await createTx({
|
|
132
|
+
asset,
|
|
133
|
+
walletAccount: MOVE_FUNDS_WALLET_ACCOUNT,
|
|
134
|
+
fromAddress,
|
|
135
|
+
toAddress,
|
|
136
|
+
amount,
|
|
137
|
+
useFeePayer: false,
|
|
138
|
+
}))
|
|
139
|
+
} catch {
|
|
140
|
+
throw new MoveFundsError('balance-negative', { fromAddress })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { fromAddress, toAddress, amount, fee, secretKey, unsignedTx }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const sendFunds = async ({ secretKey, unsignedTx }) => {
|
|
147
|
+
assert(secretKey, 'secretKey or mpcKey is required')
|
|
148
|
+
assert(unsignedTx, 'unsignedTx is required')
|
|
149
|
+
|
|
150
|
+
const importer = await getImporter(secretKey)
|
|
151
|
+
const { rawTx, txId } = await importer.sign(unsignedTx, secretKey)
|
|
152
|
+
|
|
153
|
+
await solanaApi.broadcastTransaction(rawTx)
|
|
154
|
+
|
|
155
|
+
return { txId }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { prepareSendFundsTx, sendFunds }
|
|
159
|
+
}
|