@exodus/solana-plugin 1.36.5 → 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 +10 -0
- package/package.json +3 -3
- package/src/create-asset.js +10 -0
- package/src/move-funds.js +159 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
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
|
+
|
|
6
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)
|
|
7
17
|
|
|
8
18
|
|
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
|
|
|
@@ -120,6 +121,13 @@ export const createSolanaAssetFactory =
|
|
|
120
121
|
assetClientInterface,
|
|
121
122
|
})
|
|
122
123
|
|
|
124
|
+
const moveFunds = moveFundsFactory({
|
|
125
|
+
baseAssetName,
|
|
126
|
+
solanaApi: rpcApi,
|
|
127
|
+
assetClientInterface,
|
|
128
|
+
createTx,
|
|
129
|
+
})
|
|
130
|
+
|
|
123
131
|
const stakingApi = stakingApiFactory({
|
|
124
132
|
assetName: baseAssetName,
|
|
125
133
|
assetClientInterface,
|
|
@@ -163,6 +171,7 @@ export const createSolanaAssetFactory =
|
|
|
163
171
|
family: ASSET_FAMILY.SOLANA,
|
|
164
172
|
feeMonitor: false,
|
|
165
173
|
feesApi: true,
|
|
174
|
+
moveFunds: true,
|
|
166
175
|
nfts: true,
|
|
167
176
|
staking: {},
|
|
168
177
|
isTestnet,
|
|
@@ -248,6 +257,7 @@ export const createSolanaAssetFactory =
|
|
|
248
257
|
.filter((asset) => asset.name !== base.name)
|
|
249
258
|
.map(createToken),
|
|
250
259
|
hasFeature: (feature) => !!features[feature], // @deprecated use api.features instead
|
|
260
|
+
moveFunds,
|
|
251
261
|
privateKeyEncodingDefinition: { encoding: 'base58', data: 'priv|pub' },
|
|
252
262
|
securityChecks,
|
|
253
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
|
+
}
|