@exodus/sui-api 1.3.1 → 1.4.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.
@@ -0,0 +1,66 @@
1
+ const EMPTY_EXCHANGE_RATE = Object.freeze({ sui_amount: '0', pool_token_amount: '0' })
2
+
3
+ const RATE_LOOKBACK_EPOCHS = 7
4
+
5
+ const getLookbackEpochs = (epoch) => {
6
+ const first = Math.max(0, epoch - RATE_LOOKBACK_EPOCHS)
7
+ return Array.from({ length: epoch - first + 1 }, (_, index) => epoch - index)
8
+ }
9
+
10
+ export const getRateLookupEpochs = (epochs) => [...new Set(epochs.flatMap(getLookbackEpochs))]
11
+
12
+ export const findRecordedRate = ({ rates, epoch }) =>
13
+ rates.get(getLookbackEpochs(epoch).find((candidate) => rates.has(candidate)))
14
+
15
+ const toRate = ({ sui_amount: suiAmount, pool_token_amount: poolTokenAmount }) =>
16
+ BigInt(suiAmount) === BigInt(0) ? 1 : Number(poolTokenAmount) / Number(suiAmount)
17
+
18
+ export const estimateReward = ({ principal, activationRate, currentRate }) => {
19
+ if (!currentRate) return '0'
20
+ const stakeRate = toRate(activationRate ?? EMPTY_EXCHANGE_RATE)
21
+ const reward = (stakeRate / toRate(currentRate) - 1) * Number(principal)
22
+ return String(Math.max(0, Math.round(reward)))
23
+ }
24
+
25
+ export const getLatestRateEpoch = ({ pool, epoch }) =>
26
+ pool.deactivationEpoch === null ? epoch : Math.min(epoch, pool.deactivationEpoch)
27
+
28
+ const toStake = ({ stakedSui, pool, epoch }) => {
29
+ const activationEpoch = Number(stakedSui.stake_activation_epoch)
30
+ const stake = {
31
+ stakedSuiId: stakedSui.id,
32
+ stakeRequestEpoch: String(activationEpoch - 1),
33
+ stakeActiveEpoch: String(activationEpoch),
34
+ principal: stakedSui.principal,
35
+ }
36
+
37
+ if (epoch < activationEpoch) return { ...stake, status: 'Pending' }
38
+
39
+ return {
40
+ ...stake,
41
+ status: 'Active',
42
+ estimatedReward: estimateReward({
43
+ principal: stakedSui.principal,
44
+ activationRate: pool.rates.get(activationEpoch),
45
+ currentRate: pool.currentRate,
46
+ }),
47
+ }
48
+ }
49
+
50
+ export const toDelegatedStakes = ({ stakedSuis, poolsById, epoch }) => {
51
+ const stakesByPool = new Map()
52
+ for (const stakedSui of stakedSuis) {
53
+ const pool = poolsById.get(stakedSui.pool_id)
54
+ if (!pool) throw new Error(`Cannot find rates for staking pool ${stakedSui.pool_id}`)
55
+
56
+ const delegatedStake = stakesByPool.get(stakedSui.pool_id) ?? {
57
+ validatorAddress: pool.validatorAddress,
58
+ stakingPool: stakedSui.pool_id,
59
+ stakes: [],
60
+ }
61
+ delegatedStake.stakes.push(toStake({ stakedSui, pool, epoch }))
62
+ stakesByPool.set(stakedSui.pool_id, delegatedStake)
63
+ }
64
+
65
+ return [...stakesByPool.values()]
66
+ }
@@ -1,4 +1,5 @@
1
1
  import { Transaction } from '../vendors/transactions/index.js'
2
+ import { getPureBcsSchema } from '../vendors/transactions/serializer.js'
2
3
  import {
3
4
  ADD_STAKE_FUNCTION,
4
5
  ADD_STAKE_TARGET,
@@ -49,6 +50,14 @@ const getInput = ({ inputs, argument }) =>
49
50
  ? inputs[argument.Input]
50
51
  : undefined
51
52
 
53
+ const ADDRESS_LENGTH = 32
54
+
55
+ const getAddressValue = (input) => {
56
+ if (input?.valueType !== null) return input?.value
57
+ if (input.value.length !== ADDRESS_LENGTH) return
58
+ return getPureBcsSchema('address').parse(Uint8Array.from(input.value))
59
+ }
60
+
52
61
  const getSplitAmount = ({ inputs, commands, argument }) => {
53
62
  const resultIndex = argument?.NestedResult?.[0] ?? argument?.Result
54
63
  const amountArgument = commands[resultIndex]?.SplitCoins?.[1]?.[0]
@@ -74,7 +83,7 @@ export const parseStakingCommands = (programmableTransaction) => {
74
83
  const amount = getSplitAmount({ inputs, commands, argument: coinArgument })
75
84
  return {
76
85
  operation,
77
- validator: getInput({ inputs, argument: validatorArgument })?.value,
86
+ validator: getAddressValue(getInput({ inputs, argument: validatorArgument })),
78
87
  ...(amount && { amount }),
79
88
  }
80
89
  }
@@ -61,7 +61,7 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
61
61
 
62
62
  const ourWalletAddress = await this.aci.getReceiveAddress({ assetName, walletAccount })
63
63
 
64
- const { utxos, tokenUtxos, unknownAssetIds } = await this.#getNewAccountState({
64
+ const { utxos, tokenUtxos, addressBalances, unknownAssetIds } = await this.#getNewAccountState({
65
65
  address: ourWalletAddress,
66
66
  })
67
67
 
@@ -81,6 +81,7 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
81
81
  newData: {
82
82
  utxos,
83
83
  tokenUtxos,
84
+ addressBalances,
84
85
  ...stakingData,
85
86
  },
86
87
  batch,
@@ -238,9 +239,17 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
238
239
  }
239
240
 
240
241
  #getNewAccountState = async ({ address }) => {
241
- const allCoins = await this.#serverApi.getAllCoins({
242
- owner: address,
243
- })
242
+ const [allCoins, allBalances] = await Promise.all([
243
+ this.#serverApi.getAllCoins({ owner: address }),
244
+ this.#serverApi.getAccountAllBalances({ address }),
245
+ ])
246
+
247
+ const addressBalanceByCoinType = Object.create(null)
248
+ for (const { coinType, addressBalance } of allBalances) {
249
+ if (addressBalance && addressBalance !== '0') {
250
+ addressBalanceByCoinType[coinType] = addressBalance
251
+ }
252
+ }
244
253
 
245
254
  const groupedCoins = allCoins.reduce((acc, coin) => {
246
255
  if (!acc[coin.coinType]) {
@@ -252,39 +261,43 @@ export class SuiNoHistoryMonitor extends BaseMonitor {
252
261
  }, Object.create(null))
253
262
 
254
263
  const tokenUtxos = Object.create(null)
264
+ const addressBalances = Object.create(null)
255
265
  let baseUtxos = UtxoCollection.createEmpty({
256
266
  currency: this.asset.currency,
257
267
  })
258
268
 
259
269
  const unknownAssetIds = new Set(
260
- Object.keys(groupedCoins).filter((coinType) => coinType !== coinTypes.Base)
270
+ [...Object.keys(groupedCoins), ...Object.keys(addressBalanceByCoinType)].filter(
271
+ (coinType) => coinType !== coinTypes.Base
272
+ )
261
273
  )
262
274
 
263
275
  for (const [assetName, asset] of Object.entries(this.assets)) {
264
276
  const isBaseAsset = asset.baseAssetName === asset.name
265
- if (isBaseAsset) {
266
- baseUtxos = this.#formatUtxos({
267
- address,
268
- coinType: coinTypes.Base,
269
- coins: groupedCoins[coinTypes.Base] || [],
270
- asset,
271
- })
272
- continue
273
- }
277
+ const coinType = isBaseAsset ? coinTypes.Base : asset.assetId
278
+ if (!isBaseAsset && !unknownAssetIds.delete(coinType)) continue
274
279
 
275
- if (!unknownAssetIds.delete(asset.assetId)) continue
276
-
277
- tokenUtxos[assetName] = this.#formatUtxos({
280
+ const utxos = this.#formatUtxos({
278
281
  address,
279
- coinType: asset.assetId,
280
- coins: groupedCoins[asset.assetId] || [],
282
+ coinType,
283
+ coins: groupedCoins[coinType] || [],
281
284
  asset,
282
285
  })
286
+ if (isBaseAsset) {
287
+ baseUtxos = utxos
288
+ } else {
289
+ tokenUtxos[assetName] = utxos
290
+ }
291
+
292
+ if (addressBalanceByCoinType[coinType]) {
293
+ addressBalances[assetName] = addressBalanceByCoinType[coinType]
294
+ }
283
295
  }
284
296
 
285
297
  return {
286
298
  utxos: baseUtxos,
287
299
  tokenUtxos,
300
+ addressBalances,
288
301
  unknownAssetIds: [...unknownAssetIds],
289
302
  }
290
303
  }
package/src/tx-send.js CHANGED
@@ -1,11 +1,48 @@
1
1
  import { UtxoCollection } from '@exodus/models'
2
2
  import { coinTypes, objectTypes, toCoinType } from '@exodus/sui-lib'
3
3
  import assert from 'minimalistic-assert'
4
+ import ms from 'ms'
4
5
 
6
+ import { SuiBroadcastError } from './broadcast-error.js'
5
7
  import { applyStakingTx } from './staking/staking-info.js'
6
8
  import { parseStakingCommands } from './staking/staking-transactions.js'
7
9
  import { getFeeAmount } from './transform-tx.js'
8
10
 
11
+ const LOOKUP_ATTEMPTS = 5
12
+ const LOOKUP_DELAY_MS = ms('3s')
13
+
14
+ const wait = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs))
15
+
16
+ const findExecutedTransaction = async ({ serverApi, digest, lookupAttempts, lookupDelayMs }) => {
17
+ for (let attempt = 0; attempt < lookupAttempts; attempt++) {
18
+ await wait(lookupDelayMs)
19
+ const [transaction] = await serverApi.getTransactions({ txIds: [digest] }).catch(() => [])
20
+ if (transaction) return transaction
21
+ }
22
+ }
23
+
24
+ const broadcastTransaction = async ({
25
+ asset,
26
+ plainTx,
27
+ serverApi,
28
+ lookupAttempts,
29
+ lookupDelayMs,
30
+ }) => {
31
+ try {
32
+ return await asset.baseAsset.api.broadcastTx(plainTx)
33
+ } catch (error) {
34
+ if (!(error instanceof SuiBroadcastError)) throw error
35
+ const transaction = await findExecutedTransaction({
36
+ serverApi,
37
+ digest: error.digest,
38
+ lookupAttempts,
39
+ lookupDelayMs,
40
+ })
41
+ if (!transaction) throw error
42
+ return transaction
43
+ }
44
+ }
45
+
9
46
  const deriveNewUtxos = ({ mutatedObject, balanceChange, deletedObjects, utxos, asset }) => {
10
47
  const utxoArray = utxos.toArray()
11
48
 
@@ -252,7 +289,13 @@ const getStateChanges = async ({ aci, asset, walletAccount, result }) => {
252
289
  }
253
290
  }
254
291
 
255
- export const txSendFactory = ({ assetClientInterface: aci, serverApi, createTx }) => {
292
+ export const txSendFactory = ({
293
+ assetClientInterface: aci,
294
+ serverApi,
295
+ createTx,
296
+ lookupAttempts = LOOKUP_ATTEMPTS,
297
+ lookupDelayMs = LOOKUP_DELAY_MS,
298
+ }) => {
256
299
  assert(aci, 'assetClientInterface is required')
257
300
  assert(serverApi, 'serverApi is required')
258
301
  assert(createTx, 'createTx is required')
@@ -268,7 +311,13 @@ export const txSendFactory = ({ assetClientInterface: aci, serverApi, createTx }
268
311
  walletAccount,
269
312
  })
270
313
 
271
- const result = await asset.baseAsset.api.broadcastTx(plainTx)
314
+ const result = await broadcastTransaction({
315
+ asset,
316
+ plainTx,
317
+ serverApi,
318
+ lookupAttempts,
319
+ lookupDelayMs,
320
+ })
272
321
 
273
322
  const { accountStateData: newData, txLogsByAssetName } = await getStateChanges({
274
323
  asset,
@@ -1,7 +1,7 @@
1
1
  import { vendorLib } from '@exodus/sui-lib'
2
2
 
3
- import { suiClientResolveTransactionPlugin } from '../experimental/transports/json-rpc-resolver.js'
4
3
  import { Inputs } from './Inputs.js'
4
+ import { suiClientResolveTransactionPlugin } from './resolver.js'
5
5
 
6
6
  const { bcs } = vendorLib
7
7
 
@@ -33,8 +33,7 @@ async function resolveTransactionPlugin(transactionData, options, next) {
33
33
  }
34
34
 
35
35
  const client = getClient(options)
36
- const plugin =
37
- client.core?.resolveTransactionPlugin() ?? suiClientResolveTransactionPlugin(client)
36
+ const plugin = suiClientResolveTransactionPlugin(client)
38
37
  return plugin(transactionData, options, async () => {
39
38
  await validate(transactionData)
40
39
  await next()
@@ -2,14 +2,10 @@
2
2
  /* eslint-disable no-return-await */
3
3
  import { vendorLib } from '@exodus/sui-lib'
4
4
 
5
- import { ObjectRef } from '../../transactions/data/internal.js'
6
- import { Inputs } from '../../transactions/Inputs.js'
7
- import {
8
- getPureBcsSchema,
9
- isTxContext,
10
- normalizedTypeToMoveTypeSignature,
11
- } from '../../transactions/serializer.js'
12
- import { parse } from '../../valibot.js'
5
+ import { parse } from '../valibot.js'
6
+ import { ObjectRef } from './data/internal.js'
7
+ import { Inputs } from './Inputs.js'
8
+ import { getPureBcsSchema, isTxContext } from './serializer.js'
13
9
 
14
10
  const { normalizeSuiAddress, normalizeSuiObjectId, SUI_TYPE_ARG } = vendorLib.utils
15
11
 
@@ -18,6 +14,7 @@ const { chunk } = vendorLib.utils
18
14
  const MAX_OBJECTS_PER_FETCH = 50
19
15
  const GAS_SAFE_OVERHEAD = BigInt(1000)
20
16
  const MAX_GAS = 5e10
17
+ const MAX_GAS_OBJECTS = 256
21
18
 
22
19
  function suiClientResolveTransactionPlugin(client) {
23
20
  return async function resolveTransactionData(transactionData, options, next) {
@@ -35,7 +32,7 @@ function suiClientResolveTransactionPlugin(client) {
35
32
 
36
33
  async function setGasPrice(transactionData, client) {
37
34
  if (!transactionData.gasConfig.price) {
38
- transactionData.gasConfig.price = String(await client.getReferenceGasPrice())
35
+ transactionData.gasConfig.price = await client.getReferenceGasPrice()
39
36
  }
40
37
  }
41
38
 
@@ -44,7 +41,7 @@ async function setGasBudget(transactionData, client) {
44
41
  return
45
42
  }
46
43
 
47
- const dryRunResult = await client.dryRunTransactionBlock({
44
+ const dryRunResult = await client.dryRunTransaction({
48
45
  transactionBlock: transactionData.build({
49
46
  overrides: {
50
47
  gasData: {
@@ -79,7 +76,7 @@ async function setGasPayment(transactionData, client) {
79
76
  owner: transactionData.gasConfig.owner || transactionData.sender,
80
77
  coinType: SUI_TYPE_ARG,
81
78
  })
82
- const paymentCoins = coins.data
79
+ const paymentCoins = coins
83
80
  .filter((coin) => {
84
81
  const matchingInput = transactionData.inputs.find((input) => {
85
82
  if (input.Object?.ImmOrOwnedObject) {
@@ -90,6 +87,8 @@ async function setGasPayment(transactionData, client) {
90
87
  })
91
88
  return !matchingInput
92
89
  })
90
+ .sort((a, b) => (BigInt(b.balance) > BigInt(a.balance) ? 1 : -1))
91
+ .slice(0, MAX_GAS_OBJECTS)
93
92
  .map((coin) => ({
94
93
  objectId: coin.coinObjectId,
95
94
  digest: coin.digest,
@@ -117,47 +116,13 @@ async function resolveObjectReferences(transactionData, client) {
117
116
  ]
118
117
  const objectChunks = dedupedIds.length > 0 ? chunk(dedupedIds, MAX_OBJECTS_PER_FETCH) : []
119
118
  const resolvedChunks = await Promise.all(
120
- objectChunks.map((chunk2) =>
121
- client.multiGetObjects({
122
- ids: chunk2,
123
- options: { showOwner: true },
124
- })
125
- )
126
- )
127
- const resolved = resolvedChunks.flat()
128
- const responsesById = new Map(
129
- dedupedIds.map((id, index) => {
130
- return [id, resolved[index]]
131
- })
119
+ objectChunks.map((chunk2) => client.getObjectRefs({ ids: chunk2 }))
132
120
  )
133
- const invalidObjects = [...responsesById]
134
- .filter(([_, obj]) => obj.error)
135
- .map(([_, obj]) => JSON.stringify(obj.error))
136
- if (invalidObjects.length > 0) {
137
- throw new Error(`The following input objects are invalid: ${invalidObjects.join(', ')}`)
121
+ const objects = resolvedChunks.flat()
122
+ const missing = dedupedIds.filter((id, index) => !objects[index])
123
+ if (missing.length > 0) {
124
+ throw new Error(`The following input objects are invalid: ${missing.join(', ')}`)
138
125
  }
139
-
140
- const objects = resolved.map((object) => {
141
- if (object.error || !object.data) {
142
- throw new Error(`Failed to fetch object: ${object.error}`)
143
- }
144
-
145
- const owner = object.data.owner
146
- const initialSharedVersion =
147
- owner && typeof owner === 'object'
148
- ? 'Shared' in owner
149
- ? owner.Shared.initial_shared_version
150
- : 'ConsensusAddressOwner' in owner
151
- ? owner.ConsensusAddressOwner.start_version
152
- : null
153
- : null
154
- return {
155
- objectId: object.data.objectId,
156
- digest: object.data.digest,
157
- version: object.data.version,
158
- initialSharedVersion,
159
- }
160
- })
161
126
  const objectsById = new Map(
162
127
  dedupedIds.map((id, index) => {
163
128
  return [id, objects[index]]
@@ -228,15 +193,12 @@ async function normalizeInputs(transactionData, client) {
228
193
  await Promise.all(
229
194
  [...moveFunctionsToResolve].map(async (functionName) => {
230
195
  const [packageId, moduleId, functionId] = functionName.split('::')
231
- const def = await client.getNormalizedMoveFunction({
196
+ const parameters = await client.getMoveFunctionParameters({
232
197
  package: packageId,
233
198
  module: moduleId,
234
199
  function: functionId,
235
200
  })
236
- moveFunctionParameters.set(
237
- functionName,
238
- def.parameters.map((param) => normalizedTypeToMoveTypeSignature(param))
239
- )
201
+ moveFunctionParameters.set(functionName, parameters)
240
202
  })
241
203
  )
242
204
  }
@@ -346,7 +308,7 @@ function isReceivingType(type) {
346
308
  }
347
309
 
348
310
  return (
349
- type.body.datatype.package === '0x2' &&
311
+ normalizeSuiAddress(type.body.datatype.package) === normalizeSuiAddress('0x2') &&
350
312
  type.body.datatype.module === 'transfer' &&
351
313
  type.body.datatype.type === 'Receiving'
352
314
  )