@exodus/sui-api 1.2.3 → 1.3.1

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 CHANGED
@@ -3,6 +3,31 @@
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.3.1](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.3.0...@exodus/sui-api@1.3.1) (2026-10-09)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * migrate to @exodus/basic-utils 5 ([#8894](https://github.com/ExodusMovement/assets/issues/8894)) ([ed125f3](https://github.com/ExodusMovement/assets/commit/ed125f3257785c92a3b8f8239a2e9eefe5dc6772))
12
+
13
+
14
+
15
+ ## [1.3.0](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.2.3...@exodus/sui-api@1.3.0) (2026-10-05)
16
+
17
+
18
+ ### Features
19
+
20
+ * **sui-plugin:** sui native staking via staking api v2 ([#8839](https://github.com/ExodusMovement/assets/issues/8839)) ([8523a23](https://github.com/ExodusMovement/assets/commit/8523a2303deeaa4dc4280829fb5b642191a3ae2e))
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * disable @exodus/hydra/no-param-reassign-prop-only warnings ([#8531](https://github.com/ExodusMovement/assets/issues/8531)) ([ddae556](https://github.com/ExodusMovement/assets/commit/ddae556361285919fe82cd10a81cfeae79ccc6b5))
26
+ * **lint:** fix no-await-expression-member warnings and upgrade to error ([#8503](https://github.com/ExodusMovement/assets/issues/8503)) ([a7fb2c3](https://github.com/ExodusMovement/assets/commit/a7fb2c3bcb4e6b9e97e7f8bb3b8434b78036fe68))
27
+ * **lint:** migrate lodash to @exodus/basic-utils ([#8514](https://github.com/ExodusMovement/assets/issues/8514)) ([12c574f](https://github.com/ExodusMovement/assets/commit/12c574f1ac8ab0592700df574b2643141237090a))
28
+
29
+
30
+
6
31
  ## [1.2.3](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.2.2...@exodus/sui-api@1.2.3) (2026-08-06)
7
32
 
8
33
 
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/sui-api",
3
- "packageManager": "yarn@4.3.1",
4
- "version": "1.2.3",
3
+ "version": "1.3.1",
5
4
  "description": "Transaction monitors, RPC with the blockchain node, and other networking code for Sui",
6
5
  "type": "module",
7
6
  "main": "src/index.js",
@@ -19,14 +18,12 @@
19
18
  "provenance": false
20
19
  },
21
20
  "scripts": {
22
- "test": "run -T exodus-test --jest",
23
- "lint": "run -T eslintc .",
24
- "lint:fix": "yarn lint --fix"
21
+ "test": "exodus-test --jest"
25
22
  },
26
23
  "dependencies": {
27
24
  "@exodus/asset-lib": "^5.0.0",
28
25
  "@exodus/assets": "^11.1.2",
29
- "@exodus/basic-utils": "^3.1.0",
26
+ "@exodus/basic-utils": "^5.1.0",
30
27
  "@exodus/crypto": "^1.0.0-rc.26",
31
28
  "@exodus/currency": "^6.0.1",
32
29
  "@exodus/models": "^13.0.0",
@@ -50,5 +47,5 @@
50
47
  "type": "git",
51
48
  "url": "git+https://github.com/ExodusMovement/assets.git"
52
49
  },
53
- "gitHead": "52180d93ce2bcafeaf575f9761f9863cc1bf22aa"
50
+ "gitHead": "41e6c1bb6a3d919f08cc3ee38068159259b335be"
54
51
  }
package/src/api.js CHANGED
@@ -184,11 +184,28 @@ export class SuiApi {
184
184
  return coins
185
185
  }
186
186
 
187
+ async getStakes({ owner }) {
188
+ return this.client.getStakes({ owner })
189
+ }
190
+
191
+ async getLatestSuiSystemState() {
192
+ return this.client.getLatestSuiSystemState()
193
+ }
194
+
195
+ async buildTransaction({ transaction, onlyTransactionKind = false }) {
196
+ return transaction.build({ client: this.client, onlyTransactionKind })
197
+ }
198
+
199
+ async devInspectTransaction({ sender, transactionBlock }) {
200
+ return this.client.devInspectTransactionBlock({ sender, transactionBlock })
201
+ }
202
+
187
203
  async broadcastTx(plainTx) {
188
204
  return this.client.executeTransactionBlock({
189
205
  transactionBlock: plainTx.bytes,
190
206
  signature: plainTx.signature,
191
207
  options: {
208
+ showInput: true,
192
209
  showEffects: true,
193
210
  showObjectChanges: true,
194
211
  showBalanceChanges: true,
package/src/balances.js CHANGED
@@ -1,10 +1,17 @@
1
+ import { getStakingBalances } from './staking/staking-balances.js'
2
+
1
3
  export const getBalances = ({ asset, accountState }) => {
2
4
  const isBaseAsset = asset.name === asset.baseAssetName
3
5
 
4
6
  if (isBaseAsset) {
7
+ const spendable = accountState.utxos.value
8
+ const stakingBalances = getStakingBalances({ asset, stakingInfo: accountState.stakingInfo })
9
+ const { staking, staked, unstaked } = stakingBalances
5
10
  return {
6
- total: accountState.utxos.value,
7
- spendable: accountState.utxos.value,
11
+ total: spendable.add(staking).add(staked).add(unstaked),
12
+ spendable,
13
+ stakeable: spendable,
14
+ ...stakingBalances,
8
15
  }
9
16
  }
10
17
 
@@ -1,11 +1,14 @@
1
1
  import { AccountState, UtxoCollection } from '@exodus/models'
2
2
  import { asset } from '@exodus/sui-meta'
3
3
 
4
+ import { EMPTY_STAKING_INFO } from './staking/staking-info.js'
5
+
4
6
  export const createBasicAccountState = () => {
5
7
  return class SuiAccountState extends AccountState {
6
8
  static defaults = {
7
9
  utxos: UtxoCollection.createEmpty({ currency: asset.currency }),
8
10
  tokenUtxos: Object.create(null),
11
+ stakingInfo: EMPTY_STAKING_INFO,
9
12
  }
10
13
  }
11
14
  }
package/src/index.js CHANGED
@@ -8,3 +8,7 @@ export { getFeeAsyncFactory } from './get-fee-async.js'
8
8
  export { txSendFactory } from './tx-send.js'
9
9
  export { createUnsignedTxFactory } from './create-unsigned-tx.js'
10
10
  export { createTxFactory } from './tx-create.js'
11
+ export { createStakingSettings } from './staking/staking-settings.js'
12
+ export { EVERSTAKE_VALIDATOR } from './staking/constants.js'
13
+ export { createStakingApi } from './staking/create-staking-api.js'
14
+ export { createStakingProviderClient } from './staking/staking-provider-client.js'
@@ -0,0 +1,18 @@
1
+ import ms from 'ms'
2
+
3
+ export const EVERSTAKE_VALIDATOR =
4
+ '0xbba318294a51ddeafa50c335c8e77202170e1f272599a2edc40592100863f638'
5
+ export const EVERSTAKE_VALIDATOR_NAME = 'Everstake'
6
+
7
+ export const SUI_SYSTEM_PACKAGE = '0x3'
8
+ export const SUI_SYSTEM_MODULE = 'sui_system'
9
+ export const ADD_STAKE_FUNCTION = 'request_add_stake'
10
+ export const WITHDRAW_STAKE_FUNCTION = 'request_withdraw_stake'
11
+ export const ADD_STAKE_TARGET = `${SUI_SYSTEM_PACKAGE}::${SUI_SYSTEM_MODULE}::${ADD_STAKE_FUNCTION}`
12
+ export const WITHDRAW_STAKE_TARGET = `${SUI_SYSTEM_PACKAGE}::${SUI_SYSTEM_MODULE}::${WITHDRAW_STAKE_FUNCTION}`
13
+ export const STAKED_SUI_TYPE = '0x3::staking_pool::StakedSui'
14
+
15
+ export const STAKING_GAS_BUDGET = 20_000_000
16
+ export const EPOCH_MS = ms('1d')
17
+
18
+ export const DEFAULT_STAKING_URL = 'https://staking.a.exodus.io'
@@ -0,0 +1,441 @@
1
+ import { GAS_BUDGET_SAFETY_FACTOR, MIN_GAS_BUDGET } from '@exodus/sui-lib'
2
+ import assert from 'minimalistic-assert'
3
+ import ms from 'ms'
4
+
5
+ import { getFeeAmount } from '../transform-tx.js'
6
+ import { EPOCH_MS, EVERSTAKE_VALIDATOR_NAME, STAKING_GAS_BUDGET } from './constants.js'
7
+ import { getStakeValue } from './staking-balances.js'
8
+ import { createRejectionError, createSubmitError, fail, pass } from './staking-errors.js'
9
+ import { getEpochStartMs } from './staking-info.js'
10
+ import { buildStakeTransaction, buildUnstakeTransaction } from './staking-transactions.js'
11
+
12
+ const PLAN_TTL_MS = ms('60s')
13
+ const SUPPORTED_OPERATIONS = new Set(['stake', 'unstake'])
14
+
15
+ const POSITION_STATUS = {
16
+ Pending: 'activating',
17
+ Active: 'active',
18
+ Unstaked: 'withdrawable',
19
+ }
20
+
21
+ const sum = ({ currency, amounts }) =>
22
+ amounts.reduce((total, amount) => total.add(amount), currency.ZERO)
23
+
24
+ export const createStakingApi = ({
25
+ assetClientInterface,
26
+ serverApi,
27
+ getBalances,
28
+ stakingSettings,
29
+ notify,
30
+ }) => {
31
+ assert(assetClientInterface, '`assetClientInterface` required')
32
+ assert(serverApi, '`serverApi` required')
33
+ assert(getBalances, '`getBalances` required')
34
+ assert(stakingSettings, '`stakingSettings` required')
35
+
36
+ const toValidatorTarget = (id) => ({
37
+ kind: 'validator',
38
+ id,
39
+ ...(id === stakingSettings.getValidator() && { name: EVERSTAKE_VALIDATOR_NAME }),
40
+ })
41
+
42
+ const getCapabilities = ({ asset }) => ({
43
+ participation: 'amount',
44
+ liquid: false,
45
+ positions: 'multiple',
46
+ operations: {
47
+ stake: { amount: 'required', supportsMax: true, positionSelectable: false, multiTx: false },
48
+ unstake: { amount: 'none', supportsMax: false, positionSelectable: true, multiTx: false },
49
+ },
50
+ target: {
51
+ kind: 'validator',
52
+ selection: 'fixed',
53
+ default: toValidatorTarget(stakingSettings.getValidator()),
54
+ },
55
+ unbonding: { kind: 'instant' },
56
+ claimUnstaked: 'none',
57
+ rewards: {
58
+ distribution: 'autoCompound',
59
+ approxFirstRewardMs: 2 * EPOCH_MS,
60
+ approxIntervalMs: EPOCH_MS,
61
+ },
62
+ activation: { approxDurationMs: EPOCH_MS },
63
+ feeAssetName: asset.feeAsset.name,
64
+ hardwareWallets: [],
65
+ })
66
+
67
+ const getMinStakingAmount = (feeData) => {
68
+ assert(feeData?.minStakingAmount, '`feeData.minStakingAmount` required')
69
+ return feeData.minStakingAmount
70
+ }
71
+
72
+ const estimateFee = ({ asset }) => asset.feeAsset.currency.baseUnit(STAKING_GAS_BUDGET)
73
+
74
+ const getPendingOperation = (txLog) => {
75
+ if (!txLog) return
76
+ for (const tx of txLog) {
77
+ const operation = tx.data?.staking?.operation
78
+ if (operation && tx.pending && !tx.error) return operation
79
+ }
80
+ }
81
+
82
+ const getAvailableAt = ({ stake, epoch }) =>
83
+ stake.status === 'Pending' && epoch && stake.stakeActiveEpoch !== null
84
+ ? getEpochStartMs({ epoch, targetEpoch: stake.stakeActiveEpoch })
85
+ : undefined
86
+
87
+ const getNextRewardAt = ({ stakingInfo }) => {
88
+ const { stakes, epoch } = stakingInfo
89
+ if (!epoch) return
90
+ if (stakes.some(({ status }) => status === 'Active')) {
91
+ return getEpochStartMs({ epoch, targetEpoch: epoch.epoch + 1 })
92
+ }
93
+
94
+ const activationEpochs = stakes
95
+ .filter(({ status, stakeActiveEpoch }) => status === 'Pending' && stakeActiveEpoch !== null)
96
+ .map(({ stakeActiveEpoch }) => stakeActiveEpoch)
97
+ if (activationEpochs.length === 0) return
98
+ return getEpochStartMs({ epoch, targetEpoch: Math.min(...activationEpochs) + 1 })
99
+ }
100
+
101
+ const toPosition = ({ asset, stake, epoch }) => {
102
+ const status = POSITION_STATUS[stake.status]
103
+ if (!status) throw new Error(`Unknown sui stake status ${stake.status}`)
104
+
105
+ const availableAt = getAvailableAt({ stake, epoch })
106
+ const rewards = asset.currency.baseUnit(stake.estimatedReward)
107
+ return {
108
+ id: stake.id,
109
+ status,
110
+ amount: getStakeValue({ currency: asset.currency, stake }),
111
+ target: toValidatorTarget(stake.validator),
112
+ ...(availableAt && { availableAt }),
113
+ ...(rewards.isPositive && { rewards }),
114
+ }
115
+ }
116
+
117
+ const getStakingState = ({ asset, accountState, txLog }) => {
118
+ const { stakingInfo } = accountState
119
+ const { staking, staked, unstaking, unstaked, rewards } = getBalances({ asset, accountState })
120
+ const pendingOperation = getPendingOperation(txLog)
121
+ const nextRewardAt = getNextRewardAt({ stakingInfo })
122
+
123
+ return {
124
+ loaded: stakingInfo.loaded,
125
+ isStaking: staking.add(staked).isPositive,
126
+ hasPosition: [staking, staked, unstaking, unstaked].some((amount) => amount.isPositive),
127
+ totals: { staking, staked, unstaking, unstaked, rewards },
128
+ ...(nextRewardAt && { nextRewardAt }),
129
+ positions: stakingInfo.stakes.map((stake) =>
130
+ toPosition({ asset, stake, epoch: stakingInfo.epoch })
131
+ ),
132
+ target: toValidatorTarget(stakingSettings.getValidator()),
133
+ ...(pendingOperation && { pendingOperation }),
134
+ }
135
+ }
136
+
137
+ const getMaxStakeAmount = ({ asset, spendable, fee }) =>
138
+ spendable.sub(fee).sub(estimateFee({ asset })).clampLowerZero()
139
+
140
+ const getLimits = ({ asset, accountState, feeData, operation }) => {
141
+ const { spendable, staking, staked, unstaked } = getBalances({ asset, accountState })
142
+ const estimatedFee = estimateFee({ asset })
143
+
144
+ switch (operation) {
145
+ case 'stake':
146
+ return {
147
+ min: getMinStakingAmount(feeData),
148
+ max: getMaxStakeAmount({ asset, spendable, fee: estimatedFee }),
149
+ reserve: estimatedFee,
150
+ estimatedFee,
151
+ }
152
+ case 'unstake':
153
+ return { max: staking.add(staked).add(unstaked), estimatedFee }
154
+ default:
155
+ return { max: asset.currency.ZERO }
156
+ }
157
+ }
158
+
159
+ const selectStakes = ({ stakingInfo, positionIds }) => {
160
+ if (!positionIds) return stakingInfo.stakes
161
+ const selected = stakingInfo.stakes.filter(({ id }) => positionIds.includes(id))
162
+ return selected.length === positionIds.length ? selected : []
163
+ }
164
+
165
+ const canStake = ({ asset, accountState, feeData, amount, isMax }) => {
166
+ assert(isMax || amount, '`amount` or `isMax` required to stake')
167
+ const minStakingAmount = getMinStakingAmount(feeData)
168
+ const { spendable } = getBalances({ asset, accountState })
169
+ const fee = estimateFee({ asset })
170
+ const stakeAmount = isMax ? getMaxStakeAmount({ asset, spendable, fee }) : amount
171
+
172
+ if (stakeAmount.lt(minStakingAmount)) {
173
+ return fail('BELOW_MINIMUM', minStakingAmount.sub(stakeAmount), asset.name)
174
+ }
175
+
176
+ if (spendable.lt(stakeAmount)) {
177
+ return fail('INSUFFICIENT_BALANCE', stakeAmount.sub(spendable), asset.name)
178
+ }
179
+
180
+ const required = stakeAmount.add(fee)
181
+ if (spendable.lt(required)) {
182
+ return fail('INSUFFICIENT_FEE_BALANCE', required.sub(spendable), asset.feeAsset.name)
183
+ }
184
+
185
+ return pass(spendable.sub(required).lt(estimateFee({ asset })) && 'LOW_FEE_BALANCE')
186
+ }
187
+
188
+ const canUnstake = ({ asset, accountState, positionIds }) => {
189
+ const stakes = selectStakes({ stakingInfo: accountState.stakingInfo, positionIds })
190
+ if (stakes.length === 0) return fail('NOTHING_TO_UNSTAKE')
191
+
192
+ const { spendable } = getBalances({ asset, accountState })
193
+ const fee = estimateFee({ asset })
194
+ if (spendable.lt(fee)) {
195
+ return fail('INSUFFICIENT_FEE_BALANCE', fee.sub(spendable), asset.feeAsset.name)
196
+ }
197
+
198
+ return pass()
199
+ }
200
+
201
+ const canPerform = ({
202
+ asset,
203
+ accountState,
204
+ txLog,
205
+ feeData,
206
+ operation,
207
+ amount,
208
+ isMax,
209
+ positionIds,
210
+ }) => {
211
+ if (!SUPPORTED_OPERATIONS.has(operation)) return fail('OPERATION_UNSUPPORTED')
212
+ if (!accountState.stakingInfo.loaded) return fail('NOT_LOADED')
213
+ if (getPendingOperation(txLog)) return fail('PENDING_TRANSACTION')
214
+ if (operation === 'stake') return canStake({ asset, accountState, feeData, amount, isMax })
215
+ return canUnstake({ asset, accountState, positionIds })
216
+ }
217
+
218
+ const classifyTx = ({ asset, tx }) => {
219
+ if (asset.name !== asset.baseAsset.name) return
220
+ const staking = tx.data?.staking
221
+ if (!staking) return
222
+
223
+ return {
224
+ operation: staking.operation,
225
+ ...(staking.amount && { amount: asset.currency.baseUnit(staking.amount) }),
226
+ }
227
+ }
228
+
229
+ const loadState = async ({ asset, walletAccount }) => {
230
+ const baseAssetName = asset.baseAsset.name
231
+ const [accountState, txLog, feeData, address] = await Promise.all([
232
+ assetClientInterface.getAccountState({ assetName: baseAssetName, walletAccount }),
233
+ assetClientInterface.getTxLog({ assetName: baseAssetName, walletAccount }),
234
+ assetClientInterface.getFeeData({ assetName: asset.feeAsset.name }),
235
+ assetClientInterface.getReceiveAddress({ assetName: baseAssetName, walletAccount }),
236
+ ])
237
+ return { accountState, txLog, feeData, address: address.toString() }
238
+ }
239
+
240
+ const simulateGas = async ({ asset, sender, transaction }) => {
241
+ const transactionBlock = await serverApi.buildTransaction({
242
+ transaction,
243
+ onlyTransactionKind: true,
244
+ })
245
+ const { effects } = await serverApi.devInspectTransaction({ sender, transactionBlock })
246
+ if (effects.status?.status !== 'success') {
247
+ throw new Error(`Sui staking simulation failed: ${effects.status?.error ?? 'unknown'}`)
248
+ }
249
+
250
+ const { currency } = asset.feeAsset
251
+ const fee = getFeeAmount({ gasUsed: effects.gasUsed, feeAsset: asset.feeAsset })
252
+ const computation = currency.baseUnit(effects.gasUsed.computationCost)
253
+ const charged = fee.gt(computation) ? fee : computation
254
+ const budget = Math.ceil(
255
+ Number(charged.toBaseString({ unit: false })) * GAS_BUDGET_SAFETY_FACTOR
256
+ )
257
+ return { fee, gasBudget: currency.baseUnit(Math.max(budget, MIN_GAS_BUDGET)) }
258
+ }
259
+
260
+ const reject = ({ operation, check }) => {
261
+ throw createRejectionError({ operation, check })
262
+ }
263
+
264
+ const prepareStake = async ({ asset, accountState, feeData, address, amount, isMax }) => {
265
+ const minStakingAmount = getMinStakingAmount(feeData)
266
+ const validator = stakingSettings.getValidator()
267
+ const { spendable } = getBalances({ asset, accountState })
268
+ const build = (stakeAmount) =>
269
+ buildStakeTransaction({
270
+ sender: address,
271
+ amount: stakeAmount.toBaseString({ unit: false }),
272
+ validator,
273
+ })
274
+
275
+ const estimate = await simulateGas({
276
+ asset,
277
+ sender: address,
278
+ transaction: build(isMax ? minStakingAmount : amount),
279
+ })
280
+ const stakeAmount = isMax
281
+ ? getMaxStakeAmount({ asset, spendable, fee: estimate.gasBudget })
282
+ : amount
283
+ if (stakeAmount.lt(minStakingAmount)) {
284
+ reject({
285
+ operation: 'stake',
286
+ check: fail('BELOW_MINIMUM', minStakingAmount.sub(stakeAmount), asset.name),
287
+ })
288
+ }
289
+
290
+ const required = stakeAmount.add(estimate.gasBudget)
291
+ if (spendable.lt(required)) {
292
+ reject({
293
+ operation: 'stake',
294
+ check: fail('INSUFFICIENT_FEE_BALANCE', required.sub(spendable), asset.feeAsset.name),
295
+ })
296
+ }
297
+
298
+ return {
299
+ transaction: build(stakeAmount),
300
+ ...estimate,
301
+ amount: stakeAmount,
302
+ staking: { operation: 'stake', validator, amount: stakeAmount.toBaseString({ unit: false }) },
303
+ notifyAmount: stakeAmount,
304
+ }
305
+ }
306
+
307
+ const prepareUnstake = async ({ asset, accountState, address, positionIds }) => {
308
+ const { currency } = asset
309
+ const stakes = selectStakes({ stakingInfo: accountState.stakingInfo, positionIds })
310
+ const stakedSuiIds = stakes.map(({ id }) => id)
311
+ const build = () => buildUnstakeTransaction({ sender: address, stakedSuiIds })
312
+ const estimate = await simulateGas({ asset, sender: address, transaction: build() })
313
+ const { spendable } = getBalances({ asset, accountState })
314
+ if (spendable.lt(estimate.gasBudget)) {
315
+ reject({
316
+ operation: 'unstake',
317
+ check: fail(
318
+ 'INSUFFICIENT_FEE_BALANCE',
319
+ estimate.gasBudget.sub(spendable),
320
+ asset.feeAsset.name
321
+ ),
322
+ })
323
+ }
324
+
325
+ const validator = stakingSettings.getValidator()
326
+ const notifyAmount = sum({
327
+ currency,
328
+ amounts: stakes
329
+ .filter((stake) => stake.validator === validator)
330
+ .map((stake) => currency.baseUnit(stake.principal)),
331
+ })
332
+
333
+ return {
334
+ transaction: build(),
335
+ ...estimate,
336
+ amount: sum({ currency, amounts: stakes.map((stake) => getStakeValue({ currency, stake })) }),
337
+ staking: { operation: 'unstake', validator, stakedSuiIds },
338
+ notifyAmount,
339
+ }
340
+ }
341
+
342
+ const prepare = async ({ asset, walletAccount, operation, amount, isMax, positionIds }) => {
343
+ const { accountState, txLog, feeData, address } = await loadState({ asset, walletAccount })
344
+ const check = canPerform({
345
+ asset,
346
+ accountState,
347
+ txLog,
348
+ feeData,
349
+ operation,
350
+ amount,
351
+ isMax,
352
+ positionIds,
353
+ })
354
+ if (!check.ok) reject({ operation, check })
355
+
356
+ const prepared =
357
+ operation === 'stake'
358
+ ? await prepareStake({ asset, accountState, feeData, address, amount, isMax })
359
+ : await prepareUnstake({ asset, accountState, address, positionIds })
360
+
361
+ prepared.transaction.setGasBudget(prepared.gasBudget.toBaseString({ unit: false }))
362
+ const txData = await serverApi.buildTransaction({ transaction: prepared.transaction })
363
+
364
+ return {
365
+ operation,
366
+ assetName: asset.name,
367
+ walletAccount: walletAccount.toString(),
368
+ txs: [
369
+ {
370
+ txData,
371
+ txMeta: {
372
+ staking: {
373
+ ...prepared.staking,
374
+ notifyAmount: prepared.notifyAmount.toBaseString({ unit: false }),
375
+ },
376
+ },
377
+ },
378
+ ],
379
+ submit: 'single',
380
+ amount: prepared.amount,
381
+ fee: prepared.fee,
382
+ feeAssetName: asset.feeAsset.name,
383
+ expiresAt: Date.now() + PLAN_TTL_MS,
384
+ }
385
+ }
386
+
387
+ const notifyProvider = async ({ asset, walletAccount, unsignedTx, txId }) => {
388
+ const { operation, validator, notifyAmount } = unsignedTx.txMeta.staking
389
+ if (BigInt(notifyAmount) === BigInt(0)) return
390
+ try {
391
+ const address = await assetClientInterface.getReceiveAddress({
392
+ assetName: asset.baseAsset.name,
393
+ walletAccount,
394
+ })
395
+ await notify({
396
+ assetName: asset.name,
397
+ operation,
398
+ delegator: address.toString(),
399
+ validator,
400
+ amount: notifyAmount,
401
+ txId,
402
+ })
403
+ } catch (error) {
404
+ console.error('Failed to notify staking service:', error)
405
+ }
406
+ }
407
+
408
+ const submit = async ({ asset, walletAccount, plan, onProgress }) => {
409
+ assert(plan.assetName === asset.name, 'plan was prepared for another asset')
410
+ assert(
411
+ plan.walletAccount === walletAccount.toString(),
412
+ 'plan was prepared for another wallet account'
413
+ )
414
+ if (plan.expiresAt && Date.now() > plan.expiresAt) {
415
+ reject({ operation: plan.operation, check: fail('PLAN_EXPIRED') })
416
+ }
417
+
418
+ const [unsignedTx] = plan.txs
419
+ onProgress?.({ index: 0, total: 1 })
420
+ let txId
421
+ try {
422
+ ;({ txId } = await asset.api.sendTx({ asset, walletAccount, unsignedTx }))
423
+ } catch (error) {
424
+ throw createSubmitError({ error, txIds: [], failedIndex: 0 })
425
+ }
426
+
427
+ if (notify) void notifyProvider({ asset, walletAccount, unsignedTx, txId })
428
+
429
+ return { txIds: [txId] }
430
+ }
431
+
432
+ return {
433
+ getCapabilities,
434
+ getLimits,
435
+ getStakingState,
436
+ canPerform,
437
+ classifyTx,
438
+ prepare,
439
+ submit,
440
+ }
441
+ }
@@ -0,0 +1,19 @@
1
+ const sumBy = ({ currency, stakes, toAmount }) =>
2
+ stakes.reduce((total, stake) => total.add(toAmount(stake)), currency.ZERO)
3
+
4
+ export const getStakeValue = ({ currency, stake }) =>
5
+ currency.baseUnit(stake.principal).add(currency.baseUnit(stake.estimatedReward))
6
+
7
+ export const getStakingBalances = ({ asset, stakingInfo }) => {
8
+ const { currency } = asset
9
+ const byStatus = (status) => stakingInfo.stakes.filter((stake) => stake.status === status)
10
+ const value = (stake) => getStakeValue({ currency, stake })
11
+
12
+ return {
13
+ staking: sumBy({ currency, stakes: byStatus('Pending'), toAmount: value }),
14
+ staked: sumBy({ currency, stakes: byStatus('Active'), toAmount: value }),
15
+ unstaking: currency.ZERO,
16
+ unstaked: sumBy({ currency, stakes: byStatus('Unstaked'), toAmount: value }),
17
+ rewards: currency.ZERO,
18
+ }
19
+ }
@@ -0,0 +1,27 @@
1
+ export const fail = (reasonCode, shortfall, shortfallAssetName) => ({
2
+ ok: false,
3
+ reasonCode,
4
+ ...(shortfall && { shortfall, shortfallAssetName }),
5
+ })
6
+
7
+ export const pass = (warning) => (warning ? { ok: true, warnings: [warning] } : { ok: true })
8
+
9
+ export const createRejectionError = ({ operation, check }) => {
10
+ const error = new Error(`Cannot ${operation}: ${check.reasonCode}`)
11
+ error.name = 'StakingRejectionError'
12
+ error.reasonCode = check.reasonCode
13
+ if (check.shortfall) {
14
+ error.shortfall = check.shortfall
15
+ error.shortfallAssetName = check.shortfallAssetName
16
+ }
17
+
18
+ return error
19
+ }
20
+
21
+ export const createSubmitError = ({ error, txIds, failedIndex }) => {
22
+ const submitError = new Error(error.message, { cause: error })
23
+ submitError.name = 'StakingSubmitError'
24
+ submitError.txIds = txIds
25
+ submitError.failedIndex = failedIndex
26
+ return submitError
27
+ }
@@ -0,0 +1,68 @@
1
+ import { STAKED_SUI_TYPE } from './constants.js'
2
+
3
+ const STAKE_STATUSES = new Set(['Pending', 'Active', 'Unstaked'])
4
+
5
+ export const EMPTY_STAKING_INFO = Object.freeze({ loaded: false, stakes: [], epoch: null })
6
+
7
+ const toStake = ({ validatorAddress, stake }) => {
8
+ if (!STAKE_STATUSES.has(stake.status)) {
9
+ throw new Error(`Unknown sui stake status ${stake.status}`)
10
+ }
11
+
12
+ return {
13
+ id: stake.stakedSuiId,
14
+ validator: validatorAddress,
15
+ status: stake.status,
16
+ principal: stake.principal,
17
+ estimatedReward: stake.estimatedReward ?? '0',
18
+ stakeRequestEpoch: Number(stake.stakeRequestEpoch),
19
+ stakeActiveEpoch: Number(stake.stakeActiveEpoch),
20
+ }
21
+ }
22
+
23
+ export const toEpochInfo = (systemState) => ({
24
+ epoch: Number(systemState.epoch),
25
+ startMs: Number(systemState.epochStartTimestampMs),
26
+ durationMs: Number(systemState.epochDurationMs),
27
+ })
28
+
29
+ export const isEpochExpired = ({ epoch, now }) => !epoch || now >= epoch.startMs + epoch.durationMs
30
+
31
+ export const toStakingInfo = ({ delegatedStakes, epoch }) => ({
32
+ loaded: true,
33
+ stakes: delegatedStakes.flatMap(({ validatorAddress, stakes }) =>
34
+ stakes.map((stake) => toStake({ validatorAddress, stake }))
35
+ ),
36
+ epoch,
37
+ })
38
+
39
+ export const getEpochStartMs = ({ epoch, targetEpoch }) =>
40
+ epoch.startMs + (targetEpoch - epoch.epoch) * epoch.durationMs
41
+
42
+ const isOwnedStakedSui = ({ change, ownerAddress }) =>
43
+ change.type === 'created' &&
44
+ change.objectType === STAKED_SUI_TYPE &&
45
+ change.owner?.AddressOwner === ownerAddress
46
+
47
+ export const applyStakingTx = ({ stakingInfo, staking, objectChanges, ownerAddress }) => {
48
+ if (!stakingInfo.loaded) return stakingInfo
49
+
50
+ if (staking.operation === 'unstake') {
51
+ const withdrawn = new Set(staking.stakedSuiIds)
52
+ return { ...stakingInfo, stakes: stakingInfo.stakes.filter(({ id }) => !withdrawn.has(id)) }
53
+ }
54
+
55
+ const currentEpoch = stakingInfo.epoch?.epoch
56
+ const created = objectChanges
57
+ .filter((change) => isOwnedStakedSui({ change, ownerAddress }))
58
+ .map((change) => ({
59
+ id: change.objectId,
60
+ validator: staking.validator,
61
+ status: 'Pending',
62
+ principal: staking.amount,
63
+ estimatedReward: '0',
64
+ stakeRequestEpoch: currentEpoch ?? null,
65
+ stakeActiveEpoch: currentEpoch === undefined ? null : currentEpoch + 1,
66
+ }))
67
+ return { ...stakingInfo, stakes: [...stakingInfo.stakes, ...created] }
68
+ }
@@ -0,0 +1,57 @@
1
+ import assert from 'minimalistic-assert'
2
+ import ms from 'ms'
3
+
4
+ const HTTP_POST_TIMEOUT = ms('30s')
5
+
6
+ const ACTION_BY_OPERATION = {
7
+ stake: 'start',
8
+ unstake: 'stop',
9
+ }
10
+
11
+ const MAX_ERROR_BODY_LENGTH = 200
12
+
13
+ const readErrorBody = async (response) => {
14
+ const text = await response.text()
15
+ return text.slice(0, MAX_ERROR_BODY_LENGTH)
16
+ }
17
+
18
+ const postJson = async ({ url, body, timeoutMs }) => {
19
+ const controller = new AbortController()
20
+ const timeout = setTimeout(() => controller.abort(), timeoutMs)
21
+ try {
22
+ const response = await fetch(url, {
23
+ method: 'POST',
24
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
25
+ body: JSON.stringify(body),
26
+ signal: controller.signal,
27
+ })
28
+ if (!response.ok) {
29
+ throw new Error(`${url} returned ${response.status}: ${await readErrorBody(response)}`)
30
+ }
31
+ } finally {
32
+ clearTimeout(timeout)
33
+ }
34
+ }
35
+
36
+ export const createStakingProviderClient = ({ stakingSettings, timeoutMs = HTTP_POST_TIMEOUT }) => {
37
+ assert(stakingSettings, '`stakingSettings` required')
38
+
39
+ const notify = async ({ assetName, operation, delegator, validator, amount, txId }) => {
40
+ const type = ACTION_BY_OPERATION[operation]
41
+ assert(type, `unsupported staking notify operation ${operation}`)
42
+ assert(assetName, '"assetName" is required')
43
+ assert(delegator, '"delegator" is required')
44
+ assert(validator, '"validator" is required')
45
+ assert(amount, '"amount" is required')
46
+ assert(txId, '"txId" is required')
47
+ if (!stakingSettings.isNotifyEnabled()) return
48
+
49
+ await postJson({
50
+ url: `${stakingSettings.getUrl()}/${assetName}/stake`,
51
+ body: { delegator, actions: [{ type, validator, amount, txId }] },
52
+ timeoutMs,
53
+ })
54
+ }
55
+
56
+ return { notify }
57
+ }
@@ -0,0 +1,39 @@
1
+ import { isValidAddress } from '@exodus/sui-lib'
2
+
3
+ import { DEFAULT_STAKING_URL, EVERSTAKE_VALIDATOR } from './constants.js'
4
+
5
+ const assertValidator = (validator) => {
6
+ if (!isValidAddress(validator)) throw new Error(`Invalid sui staking validator ${validator}`)
7
+ return validator
8
+ }
9
+
10
+ const assertUrl = (url) => new URL(url).toString().replace(/\/$/, '')
11
+
12
+ export const createStakingSettings = (
13
+ { validator = EVERSTAKE_VALIDATOR, notify = Object.create(null) } = Object.create(null)
14
+ ) => {
15
+ const localNotifyEnabled = notify.enabled === true
16
+ const defaults = {
17
+ validator: assertValidator(validator),
18
+ notifyEnabled: localNotifyEnabled,
19
+ url: assertUrl(notify.url ?? DEFAULT_STAKING_URL),
20
+ }
21
+ let current = { ...defaults }
22
+
23
+ const applyRemoteConfig = (staking) => {
24
+ const remote = staking ?? Object.create(null)
25
+ current = {
26
+ validator: remote.validator ? assertValidator(remote.validator) : defaults.validator,
27
+ notifyEnabled: localNotifyEnabled && remote.notifyEnabled !== false,
28
+ url: remote.url ? assertUrl(remote.url) : defaults.url,
29
+ }
30
+ }
31
+
32
+ return {
33
+ isNotifyConfigured: localNotifyEnabled,
34
+ getValidator: () => current.validator,
35
+ isNotifyEnabled: () => current.notifyEnabled,
36
+ getUrl: () => current.url,
37
+ applyRemoteConfig,
38
+ }
39
+ }
@@ -0,0 +1,88 @@
1
+ import { Transaction } from '../vendors/transactions/index.js'
2
+ import {
3
+ ADD_STAKE_FUNCTION,
4
+ ADD_STAKE_TARGET,
5
+ SUI_SYSTEM_MODULE,
6
+ SUI_SYSTEM_PACKAGE,
7
+ WITHDRAW_STAKE_FUNCTION,
8
+ WITHDRAW_STAKE_TARGET,
9
+ } from './constants.js'
10
+
11
+ const OPERATION_BY_FUNCTION = {
12
+ [ADD_STAKE_FUNCTION]: 'stake',
13
+ [WITHDRAW_STAKE_FUNCTION]: 'unstake',
14
+ }
15
+
16
+ export const buildStakeTransaction = ({ sender, amount, validator }) => {
17
+ const tx = new Transaction()
18
+ const [stakeCoin] = tx.splitCoins(tx.gas, [amount])
19
+ tx.moveCall({
20
+ target: ADD_STAKE_TARGET,
21
+ arguments: [tx.object.system(), stakeCoin, tx.pure.address(validator)],
22
+ })
23
+ tx.setSender(sender)
24
+ return tx
25
+ }
26
+
27
+ export const buildUnstakeTransaction = ({ sender, stakedSuiIds }) => {
28
+ if (stakedSuiIds.length === 0) throw new Error('stakedSuiIds required to unstake')
29
+
30
+ const tx = new Transaction()
31
+ for (const stakedSuiId of stakedSuiIds) {
32
+ tx.moveCall({
33
+ target: WITHDRAW_STAKE_TARGET,
34
+ arguments: [tx.object.system(), tx.object(stakedSuiId)],
35
+ })
36
+ }
37
+
38
+ tx.setSender(sender)
39
+ return tx
40
+ }
41
+
42
+ const isSuiSystemCall = (moveCall) =>
43
+ BigInt(moveCall.package) === BigInt(SUI_SYSTEM_PACKAGE) &&
44
+ moveCall.module === SUI_SYSTEM_MODULE &&
45
+ Boolean(OPERATION_BY_FUNCTION[moveCall.function])
46
+
47
+ const getInput = ({ inputs, argument }) =>
48
+ argument && typeof argument === 'object' && 'Input' in argument
49
+ ? inputs[argument.Input]
50
+ : undefined
51
+
52
+ const getSplitAmount = ({ inputs, commands, argument }) => {
53
+ const resultIndex = argument?.NestedResult?.[0] ?? argument?.Result
54
+ const amountArgument = commands[resultIndex]?.SplitCoins?.[1]?.[0]
55
+ return getInput({ inputs, argument: amountArgument })?.value
56
+ }
57
+
58
+ export const parseStakingCommands = (programmableTransaction) => {
59
+ if (programmableTransaction?.kind !== 'ProgrammableTransaction') return
60
+
61
+ const { inputs, transactions: commands } = programmableTransaction
62
+ const moveCalls = commands
63
+ .map((command) => command.MoveCall)
64
+ .filter((moveCall) => moveCall && isSuiSystemCall(moveCall))
65
+ if (moveCalls.length === 0) return
66
+
67
+ const operations = new Set(moveCalls.map((moveCall) => OPERATION_BY_FUNCTION[moveCall.function]))
68
+ if (operations.size > 1) return
69
+
70
+ const [operation] = operations
71
+ if (operation === 'stake') {
72
+ if (moveCalls.length > 1) return
73
+ const [, coinArgument, validatorArgument] = moveCalls[0].arguments
74
+ const amount = getSplitAmount({ inputs, commands, argument: coinArgument })
75
+ return {
76
+ operation,
77
+ validator: getInput({ inputs, argument: validatorArgument })?.value,
78
+ ...(amount && { amount }),
79
+ }
80
+ }
81
+
82
+ return {
83
+ operation,
84
+ stakedSuiIds: moveCalls.map(
85
+ (moveCall) => getInput({ inputs, argument: moveCall.arguments[1] }).objectId
86
+ ),
87
+ }
88
+ }
@@ -6,6 +6,7 @@ import lodash from 'lodash'
6
6
  import assert from 'minimalistic-assert'
7
7
  import ms from 'ms'
8
8
 
9
+ import { isEpochExpired, toEpochInfo, toStakingInfo } from './staking/staking-info.js'
9
10
  import { transformTransactions } from './transform-tx.js'
10
11
 
11
12
  const { isEmpty, unionBy, zipObject } = lodash
@@ -14,8 +15,20 @@ export const UNCONFIRMED_TX_LIMIT = ms('5m')
14
15
 
15
16
  export class SuiNoHistoryMonitor extends BaseMonitor {
16
17
  #serverApi
17
-
18
- constructor({ asset, serverApi, interval = ms('15s'), assetClientInterface, runner, logger }) {
18
+ #stakingSettings
19
+ #stakingEnabled
20
+ #epoch = null
21
+
22
+ constructor({
23
+ asset,
24
+ serverApi,
25
+ stakingSettings,
26
+ stakingEnabled = false,
27
+ interval = ms('15s'),
28
+ assetClientInterface,
29
+ runner,
30
+ logger,
31
+ }) {
19
32
  super({
20
33
  asset,
21
34
  interval,
@@ -24,8 +37,11 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
24
37
  runner,
25
38
  })
26
39
  assert(serverApi, 'Sui Node API Client is required!')
40
+ assert(stakingSettings, 'stakingSettings is required')
27
41
 
28
42
  this.#serverApi = serverApi
43
+ this.#stakingSettings = stakingSettings
44
+ this.#stakingEnabled = stakingEnabled
29
45
  this.assets = Object.create(null)
30
46
  }
31
47
 
@@ -35,6 +51,8 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
35
51
  this.#serverApi.setServer({
36
52
  rpcUrl: remoteConfig?.rpcUrl || RPC_URL,
37
53
  })
54
+ if (!this.#stakingEnabled) return
55
+ this.#stakingSettings.applyRemoteConfig(remoteConfig?.staking)
38
56
  }
39
57
 
40
58
  async tick({ refresh, walletAccount }) {
@@ -51,6 +69,10 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
51
69
  this.emit('unknown-tokens', unknownAssetIds)
52
70
  }
53
71
 
72
+ const stakingData = this.#stakingEnabled
73
+ ? { stakingInfo: await this.#getStakingInfo({ address: ourWalletAddress }) }
74
+ : Object.create(null)
75
+
54
76
  let batch = this.aci.createOperationsBatch()
55
77
 
56
78
  batch = this.aci.updateAccountStateBatch({
@@ -59,6 +81,7 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
59
81
  newData: {
60
82
  utxos,
61
83
  tokenUtxos,
84
+ ...stakingData,
62
85
  },
63
86
  batch,
64
87
  })
@@ -87,6 +110,22 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
87
110
  await this.aci.executeOperationsBatch(batch)
88
111
  }
89
112
 
113
+ #getStakingInfo = async ({ address }) => {
114
+ const [delegatedStakes, epoch] = await Promise.all([
115
+ this.#serverApi.getStakes({ owner: address }),
116
+ this.#getEpoch(),
117
+ ])
118
+ return toStakingInfo({ delegatedStakes, epoch })
119
+ }
120
+
121
+ #getEpoch = async () => {
122
+ if (isEpochExpired({ epoch: this.#epoch, now: SynchronizedTime.now() })) {
123
+ this.#epoch = toEpochInfo(await this.#serverApi.getLatestSuiSystemState())
124
+ }
125
+
126
+ return this.#epoch
127
+ }
128
+
90
129
  #checkPendingTransactions = ({ pendingTransactions, pendingTxsFromNode }) => {
91
130
  const txsToUpdate = []
92
131
  const txsToRemove = []
@@ -1,3 +1,5 @@
1
+ import { parseStakingCommands } from './staking/staking-transactions.js'
2
+
1
3
  const parseTransaction = ({ transaction, asset }) => {
2
4
  const { data } = transaction
3
5
  const {
@@ -68,6 +70,10 @@ export function transformTransactions({ transactionResults, address, feeAsset, a
68
70
  const { gasUsed } = effects
69
71
 
70
72
  const { from, to, coinAmount } = parseTransaction({ transaction, asset })
73
+ const staking =
74
+ asset.name === asset.baseAssetName
75
+ ? parseStakingCommands(transaction.data.transaction)
76
+ : undefined
71
77
  const { confirmations, error } = parseConfirmations({ effects })
72
78
  const wasSend = from === address
73
79
  const args = Object.create(null)
@@ -88,7 +94,7 @@ export function transformTransactions({ transactionResults, address, feeAsset, a
88
94
  selfSend: wasSend && from === to,
89
95
  confirmations,
90
96
  error,
91
- data: Object.create(null),
97
+ data: staking ? { staking } : Object.create(null),
92
98
  currencies: {
93
99
  [asset.name]: asset.currency,
94
100
  [feeAsset.name]: feeAsset.currency,
package/src/tx-send.js CHANGED
@@ -2,6 +2,8 @@ import { UtxoCollection } from '@exodus/models'
2
2
  import { coinTypes, objectTypes, toCoinType } from '@exodus/sui-lib'
3
3
  import assert from 'minimalistic-assert'
4
4
 
5
+ import { applyStakingTx } from './staking/staking-info.js'
6
+ import { parseStakingCommands } from './staking/staking-transactions.js'
5
7
  import { getFeeAmount } from './transform-tx.js'
6
8
 
7
9
  const deriveNewUtxos = ({ mutatedObject, balanceChange, deletedObjects, utxos, asset }) => {
@@ -186,6 +188,10 @@ const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
186
188
 
187
189
  newAccountState.utxos = newBaseUtxos.utxos
188
190
 
191
+ const parsedStaking = wasBaseAssetSent
192
+ ? parseStakingCommands(result.transaction?.data?.transaction)
193
+ : undefined
194
+
189
195
  let amount
190
196
  if (wasBaseAssetSent) {
191
197
  amount = asset.currency.baseUnit(ownerBalanceChangesByType[coinTypes.Base]).add(fee)
@@ -203,23 +209,34 @@ const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
203
209
  amount = asset.currency.baseUnit(ownerBalanceChangesByType[asset.assetId])
204
210
  }
205
211
 
206
- const to = getRecipientAddress({ balanceChanges, ownerAddress })
212
+ const staking = parsedStaking && { ...parsedStaking, amount: amount.abs().toBaseString() }
213
+ const isUnstake = staking?.operation === 'unstake'
214
+ const to = staking ? staking.validator : getRecipientAddress({ balanceChanges, ownerAddress })
207
215
 
208
216
  const tx = {
209
217
  txId: digest,
210
218
  confirmations: 0,
211
219
  coinName: assetName,
212
- coinAmount: amount.abs().negate(),
220
+ coinAmount: isUnstake ? amount.abs() : amount.abs().negate(),
213
221
  feeAmount: fee,
214
222
  feeCoinName: feeAsset.name,
215
223
  selfSend: false,
216
- to,
217
- data: Object.create(null),
224
+ ...(to && { to }),
225
+ ...(isUnstake && { from: [ownerAddress] }),
226
+ data: staking ? { staking } : Object.create(null),
218
227
  currencies: { [assetName]: assetCurrency, [feeAsset.name]: feeAsset.currency },
219
228
  }
220
229
 
221
230
  return {
222
231
  accountStateData: {
232
+ ...(staking && {
233
+ stakingInfo: applyStakingTx({
234
+ stakingInfo: accountState.stakingInfo,
235
+ staking,
236
+ objectChanges,
237
+ ownerAddress,
238
+ }),
239
+ }),
223
240
  utxos: newAccountState.utxos,
224
241
  tokenUtxos: {
225
242
  ...accountState.tokenUtxos,
@@ -5,7 +5,8 @@ var __typeError = (msg) => {
5
5
 
6
6
  var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError('Cannot ' + msg)
7
7
  var __privateGet = (obj, member, getter) => (
8
- __accessCheck(obj, member, 'read from private field'), getter ? getter.call(obj) : member.get(obj)
8
+ __accessCheck(obj, member, 'read from private field'),
9
+ getter ? getter.call(obj) : member.get(obj)
9
10
  )
10
11
  var __privateAdd = (obj, member, value) =>
11
12
  member.has(obj)
@@ -1,4 +1,4 @@
1
- /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
1
+ /* eslint-disable @exodus/hydra/no-param-reassign-prop-only */
2
2
  /* eslint-disable no-return-await */
3
3
  import { vendorLib } from '@exodus/sui-lib'
4
4
 
@@ -116,18 +116,15 @@ async function resolveObjectReferences(transactionData, client) {
116
116
  ),
117
117
  ]
118
118
  const objectChunks = dedupedIds.length > 0 ? chunk(dedupedIds, MAX_OBJECTS_PER_FETCH) : []
119
- const resolved = (
120
- await Promise.all(
121
- objectChunks.map((chunk2) =>
122
- client.multiGetObjects({
123
- ids: chunk2,
124
- options: { showOwner: true },
125
- })
126
- )
119
+ const resolvedChunks = await Promise.all(
120
+ objectChunks.map((chunk2) =>
121
+ client.multiGetObjects({
122
+ ids: chunk2,
123
+ options: { showOwner: true },
124
+ })
127
125
  )
128
126
  )
129
- // eslint-disable-next-line unicorn/no-await-expression-member
130
- .flat()
127
+ const resolved = resolvedChunks.flat()
131
128
  const responsesById = new Map(
132
129
  dedupedIds.map((id, index) => {
133
130
  return [id, resolved[index]]
@@ -1,4 +1,4 @@
1
- /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
1
+ /* eslint-disable @exodus/hydra/no-param-reassign-prop-only */
2
2
  import { vendorLib } from '@exodus/sui-lib'
3
3
 
4
4
  import { parse } from '../valibot.js'
@@ -22,7 +22,8 @@ var __typeError = (msg) => {
22
22
 
23
23
  var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError('Cannot ' + msg)
24
24
  var __privateGet = (obj, member, getter) => (
25
- __accessCheck(obj, member, 'read from private field'), getter ? getter.call(obj) : member.get(obj)
25
+ __accessCheck(obj, member, 'read from private field'),
26
+ getter ? getter.call(obj) : member.get(obj)
26
27
  )
27
28
  var __privateAdd = (obj, member, value) =>
28
29
  member.has(obj)
@@ -36,7 +37,8 @@ var __privateSet = (obj, member, value, setter) => (
36
37
  value
37
38
  )
38
39
  var __privateMethod = (obj, member, method) => (
39
- __accessCheck(obj, member, 'access private method'), method
40
+ __accessCheck(obj, member, 'access private method'),
41
+ method
40
42
  )
41
43
  var _serializationPlugins,
42
44
  _buildPlugins,
@@ -1,4 +1,4 @@
1
- /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
1
+ /* eslint-disable @exodus/hydra/no-param-reassign-prop-only */
2
2
  import { vendorLib } from '@exodus/sui-lib'
3
3
 
4
4
  import { parse } from '../valibot.js'
@@ -1,4 +1,3 @@
1
- // eslint-disable-next-line @exodus/import/no-deprecated
2
1
  import { hashSync } from '@exodus/crypto/hash'
3
2
 
4
3
  function hashTypedData(typeTag, data) {
@@ -7,7 +6,6 @@ function hashTypedData(typeTag, data) {
7
6
  const dataWithTag = new Uint8Array(typeTagBytes.length + data.length)
8
7
  dataWithTag.set(typeTagBytes)
9
8
  dataWithTag.set(data, typeTagBytes.length)
10
- // eslint-disable-next-line @exodus/import/no-deprecated
11
9
  return hashSync('blake2b256', dataWithTag)
12
10
  }
13
11
 
@@ -88,7 +88,7 @@ function normalizeRawArgument(arg, schema, transactionData) {
88
88
  return
89
89
  }
90
90
 
91
- // eslint-disable-next-line @exodus/mutable/no-param-reassign-prop-only
91
+ // eslint-disable-next-line @exodus/hydra/no-param-reassign-prop-only
92
92
  transactionData.inputs[arg.Input] = Inputs.Pure(schema.serialize(input.UnresolvedPure.value))
93
93
  }
94
94
 
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable unicorn/no-await-expression-member */
2
2
  /* eslint-disable sonarjs/no-nested-template-literals */
3
- /* eslint-disable @exodus/mutable/no-param-reassign-prop-only */
3
+ /* eslint-disable @exodus/hydra/no-param-reassign-prop-only */
4
4
  // src/storages/globalConfig/globalConfig.ts
5
5
 
6
6
  // @__NO_SIDE_EFFECTS__