@exodus/sui-api 1.3.0 → 1.4.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 CHANGED
@@ -3,6 +3,24 @@
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.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.3.1...@exodus/sui-api@1.4.0) (2026-10-22)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **sui-api:** migrate node calls from json-rpc to graphql ([#9043](https://github.com/ExodusMovement/assets/issues/9043)) ([d228aba](https://github.com/ExodusMovement/assets/commit/d228abaa1cfa991ff03d429b9c1a49ffd825065b))
12
+
13
+
14
+
15
+ ## [1.3.1](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.3.0...@exodus/sui-api@1.3.1) (2026-10-09)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * migrate to @exodus/basic-utils 5 ([#8894](https://github.com/ExodusMovement/assets/issues/8894)) ([ed125f3](https://github.com/ExodusMovement/assets/commit/ed125f3257785c92a3b8f8239a2e9eefe5dc6772))
21
+
22
+
23
+
6
24
  ## [1.3.0](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.2.3...@exodus/sui-api@1.3.0) (2026-10-05)
7
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/sui-api",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Transaction monitors, RPC with the blockchain node, and other networking code for Sui",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -22,21 +22,17 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@exodus/asset-lib": "^5.0.0",
25
- "@exodus/assets": "^11.1.2",
26
- "@exodus/basic-utils": "^3.1.0",
25
+ "@exodus/basic-utils": "^5.1.0",
27
26
  "@exodus/crypto": "^1.0.0-rc.26",
28
27
  "@exodus/currency": "^6.0.1",
29
28
  "@exodus/models": "^13.0.0",
30
29
  "@exodus/sui-lib": "^1.3.0",
31
30
  "@exodus/sui-meta": "^1.2.0",
32
- "bn.js": "^5.2.1",
33
31
  "lodash": "^4.17.21",
34
32
  "minimalistic-assert": "^1.0.1",
35
- "ms": "^2.1.3",
36
- "url-join": "^4.0.0"
33
+ "ms": "^2.1.3"
37
34
  },
38
35
  "devDependencies": {
39
- "@exodus/asset": "^2.3.0",
40
36
  "@exodus/assets-testing": "^1.0.0"
41
37
  },
42
38
  "homepage": "https://github.com/ExodusMovement/assets/tree/master/sui/sui-api",
@@ -47,5 +43,5 @@
47
43
  "type": "git",
48
44
  "url": "git+https://github.com/ExodusMovement/assets.git"
49
45
  },
50
- "gitHead": "cde4077e979a0c4715777b654912fc29353849a9"
46
+ "gitHead": "f210c7a3aa55561320c43aa8d8d5804b49f390d0"
51
47
  }
package/src/api.js CHANGED
@@ -1,215 +1,465 @@
1
- import { RPC_URL } from '@exodus/sui-lib'
2
- import assert from 'minimalistic-assert'
3
-
4
- import { SuiClient } from './vendors/client/index.js'
5
-
6
- const getTextFromResponse = async (response) => {
7
- try {
8
- const responseBody = await response.text()
9
- return responseBody.slice(0, 100)
10
- } catch {
11
- return ''
12
- }
13
- }
14
-
15
- const fetchJson = async (url, fetchOptions) => {
16
- const response = await fetch(url, fetchOptions)
17
-
18
- if (!response.ok) {
19
- throw new Error(
20
- `${url} returned ${response.status}: ${
21
- response.statusText || 'Unknown Status Text'
22
- }. Body: ${await getTextFromResponse(response)}`
23
- )
24
- }
25
-
26
- return response.json()
27
- }
1
+ import { SynchronizedTime } from '@exodus/basic-utils'
2
+ import { coinTypes, RPC_URL, vendorLib } from '@exodus/sui-lib'
3
+ import lodash from 'lodash'
4
+
5
+ import { SuiBroadcastError } from './broadcast-error.js'
6
+ import {
7
+ GraphQLClient,
8
+ GraphQLTimeoutError,
9
+ GraphQLTransportError,
10
+ } from './graphql/graphql-client.js'
11
+ import {
12
+ getMoveCalls,
13
+ toCoin,
14
+ toJsonRpcTypeTag,
15
+ toMoveFunctionKey,
16
+ toObjectRef,
17
+ toSimulationEffects,
18
+ toTransactionBlockResponse,
19
+ } from './graphql/json-rpc-shapes.js'
20
+ import {
21
+ ACTIVE_VALIDATOR_POOLS,
22
+ ALL_BALANCES,
23
+ BALANCE,
24
+ EXCHANGE_RATES,
25
+ EXECUTE_TRANSACTION,
26
+ INACTIVE_VALIDATOR_WRAPPER,
27
+ LATEST_EPOCH,
28
+ MOVE_FUNCTION_PARAMETERS,
29
+ MULTI_GET_OBJECTS,
30
+ MULTI_GET_TRANSACTIONS,
31
+ OWNED_OBJECTS,
32
+ SIMULATE_TRANSACTION,
33
+ VERSIONED_VALIDATOR,
34
+ } from './graphql/queries.js'
35
+ import { STAKED_SUI_TYPE } from './staking/constants.js'
36
+ import {
37
+ findRecordedRate,
38
+ getLatestRateEpoch,
39
+ getRateLookupEpochs,
40
+ toDelegatedStakes,
41
+ } from './staking/delegated-stakes.js'
42
+ import { TransactionDataBuilder } from './vendors/transactions/TransactionData.js'
43
+
44
+ const { bcs } = vendorLib
45
+ const { fromBase64, fromHex, toBase64 } = vendorLib.utils
46
+ const { chunk } = lodash
47
+
48
+ const COIN_TYPE = '0x2::coin::Coin'
49
+ const MAX_DIGESTS_PER_QUERY = 50
50
+ const MAX_KEYS_PER_QUERY = 50
51
+ const DEV_INSPECT_GAS_BUDGET = '50000000000'
52
+ const EXECUTE_TIMEOUT_MS = 60_000
28
53
 
29
54
  export class SuiApi {
55
+ #moveFunctionParameters
56
+ #epoch
57
+ #activePools
58
+ #inactivePools
59
+ #exchangeRates
60
+
30
61
  constructor({ rpcUrl }) {
31
62
  this.setServer({ rpcUrl })
32
63
  }
33
64
 
34
65
  setServer({ rpcUrl = RPC_URL }) {
66
+ this.#moveFunctionParameters = new Map()
67
+ this.#epoch = null
68
+ this.#activePools = null
69
+ this.#inactivePools = new Map()
70
+ this.#exchangeRates = new Map()
35
71
  this.rpcUrl = encodeURI(rpcUrl)
36
- this.client = new SuiClient({
37
- url: rpcUrl,
38
- })
72
+ const separator = this.rpcUrl.endsWith('/') ? '' : '/'
73
+ this.graphql = new GraphQLClient({ url: `${this.rpcUrl}${separator}graphql` })
39
74
  }
40
75
 
41
- handleJsonRPCResponse(response) {
42
- const result = response?.result
43
- const error = response?.error
44
- if (error || result === undefined) {
45
- const message = error?.message || error?.code || 'no result'
46
- throw new Error(`Bad rpc response: ${message}`)
47
- }
48
-
49
- return result
76
+ async #query(query, variables, options) {
77
+ return this.graphql.query({ query, variables, ...options })
50
78
  }
51
79
 
52
- async request({ baseApiPath, path = '', method, body, headers = Object.create(null) }) {
53
- assert(typeof baseApiPath === 'string', 'expected string baseApiPath')
80
+ async #paginate({ query, variables, getConnection }) {
81
+ const nodes = []
82
+ let cursor = null
83
+ while (true) {
84
+ const connection = getConnection(await this.#query(query, { ...variables, cursor }))
85
+ if (!connection) return nodes
86
+ nodes.push(...connection.nodes)
87
+ if (!connection.pageInfo.hasNextPage) return nodes
88
+ cursor = connection.pageInfo.endCursor
89
+ }
90
+ }
54
91
 
55
- const url = new URL(`${baseApiPath}${path}`)
56
- const fetchOptions = {
57
- method,
58
- headers: { ...this.getDefaultHeaders(), ...headers },
92
+ async #findMoveFunctionParameters({ package: pkg, module, function: fn }) {
93
+ const key = toMoveFunctionKey({ package: pkg, module, function: fn })
94
+ if (!this.#moveFunctionParameters.has(key)) {
95
+ const request = this.#query(MOVE_FUNCTION_PARAMETERS, {
96
+ package: pkg,
97
+ module,
98
+ function: fn,
99
+ }).then((data) => {
100
+ const parameters = data.package?.module?.function?.parameters
101
+ if (!parameters) {
102
+ this.#moveFunctionParameters.delete(key)
103
+ return null
104
+ }
105
+
106
+ return parameters.map(({ signature }) => ({
107
+ ref: signature.ref ?? null,
108
+ body: signature.body,
109
+ }))
110
+ })
111
+ this.#moveFunctionParameters.set(key, request)
112
+ request.catch(() => this.#moveFunctionParameters.delete(key))
59
113
  }
60
114
 
61
- if (body) fetchOptions.body = JSON.stringify(body)
62
- const response = await fetchJson(url, fetchOptions)
63
- return this.handleJsonRPCResponse(response)
115
+ return this.#moveFunctionParameters.get(key)
64
116
  }
65
117
 
66
- getDefaultHeaders() {
67
- return {
68
- Accept: 'application/json',
69
- 'Content-Type': 'application/json',
118
+ async getMoveFunctionParameters(moveFunction) {
119
+ const parameters = await this.#findMoveFunctionParameters(moveFunction)
120
+ if (!parameters) {
121
+ throw new Error(`Cannot find sui move function ${toMoveFunctionKey(moveFunction)}`)
70
122
  }
123
+
124
+ return parameters
71
125
  }
72
126
 
73
- // Transactions
127
+ async #getMoveFunctionParametersByKey({ transactionJsons, tolerateLookupErrors }) {
128
+ const moveCalls = lodash.uniqBy(transactionJsons.flatMap(getMoveCalls), toMoveFunctionKey)
129
+ const entries = await Promise.all(
130
+ moveCalls.map(async (moveCall) => {
131
+ const lookup = this.#findMoveFunctionParameters(moveCall)
132
+ return [
133
+ toMoveFunctionKey(moveCall),
134
+ await (tolerateLookupErrors ? lookup.catch(() => null) : lookup),
135
+ ]
136
+ })
137
+ )
138
+ return new Map(entries.filter(([, parameters]) => parameters))
139
+ }
74
140
 
75
- async getTransactionByHash({ hash }) {
76
- return this.request({
77
- baseApiPath: this.rpcUrl,
78
- method: 'POST',
79
- body: {
80
- jsonrpc: '2.0',
81
- id: 1,
82
- method: 'sui_getTransactionBlock',
83
- params: [
84
- hash,
85
- {
86
- showInput: true,
87
- showRawInput: false,
88
- showEffects: true,
89
- showEvents: false,
90
- showObjectChanges: true,
91
- showBalanceChanges: true,
92
- showRawEffects: false,
93
- },
94
- ],
95
- },
141
+ async #toTransactionBlockResponses(transactions, { tolerateLookupErrors = false } = {}) {
142
+ const moveFunctionParameters = await this.#getMoveFunctionParametersByKey({
143
+ transactionJsons: transactions.map(({ transactionJson }) => transactionJson),
144
+ tolerateLookupErrors,
96
145
  })
146
+ return transactions.map((transaction) =>
147
+ toTransactionBlockResponse({ ...transaction, moveFunctionParameters })
148
+ )
149
+ }
150
+
151
+ async getTransactionByHash({ hash }) {
152
+ const [transaction] = await this.getTransactions({ txIds: [hash] })
153
+ if (!transaction) throw new Error(`Sui transaction ${hash} not found`)
154
+ return transaction
155
+ }
156
+
157
+ async getTransactions({ txIds }) {
158
+ const results = await Promise.all(
159
+ chunk(txIds, MAX_DIGESTS_PER_QUERY).map((digests) =>
160
+ this.#query(MULTI_GET_TRANSACTIONS, { digests })
161
+ )
162
+ )
163
+ const transactions = results
164
+ .flatMap((data) => data.multiGetTransactions)
165
+ .filter((transaction) => transaction?.effects)
166
+ return this.#toTransactionBlockResponses(transactions)
97
167
  }
98
168
 
99
169
  async getAccountBalance({ address }) {
100
- return this.request({
101
- baseApiPath: this.rpcUrl,
102
- method: 'POST',
103
- body: {
104
- jsonrpc: '2.0',
105
- id: 1,
106
- method: 'suix_getBalance',
107
- params: [address],
108
- },
109
- })
170
+ const data = await this.#query(BALANCE, { owner: address, coinType: coinTypes.Base })
171
+ const balance = data.address?.balance
172
+ return {
173
+ coinType: coinTypes.Base,
174
+ totalBalance: balance?.totalBalance ?? '0',
175
+ }
110
176
  }
111
177
 
112
178
  async getAccountAllBalances({ address }) {
113
- return this.request({
114
- baseApiPath: this.rpcUrl,
115
- method: 'POST',
116
- body: {
117
- jsonrpc: '2.0',
118
- id: 1,
119
- method: 'suix_getAllBalances',
120
- params: [address],
121
- },
179
+ const balances = await this.#paginate({
180
+ query: ALL_BALANCES,
181
+ variables: { owner: address },
182
+ getConnection: (data) => data.address?.balances,
122
183
  })
184
+ return balances.map(({ coinType, totalBalance }) => ({
185
+ coinType: toJsonRpcTypeTag(coinType.repr),
186
+ totalBalance,
187
+ }))
123
188
  }
124
189
 
125
- async getTransactions({ txIds }) {
126
- return this.client.multiGetTransactionBlocks({
127
- digests: txIds,
128
- options: {
129
- showEffects: true,
130
- showObjectChanges: true,
131
- showBalanceChanges: true,
132
- showInput: true,
133
- },
190
+ async #getOwnedObjects({ owner, type }) {
191
+ return this.#paginate({
192
+ query: OWNED_OBJECTS,
193
+ variables: { owner, type },
194
+ getConnection: (data) => data.address?.objects,
134
195
  })
135
196
  }
136
197
 
137
198
  async getAllCoins({ owner }) {
138
- const requestByCursor = async ({ cursor }) => {
139
- return this.client.getAllCoins({
140
- owner,
141
- cursor,
142
- limit: 100,
143
- })
144
- }
199
+ const objects = await this.#getOwnedObjects({ owner, type: COIN_TYPE })
200
+ return objects.map(toCoin)
201
+ }
145
202
 
146
- const coins = []
147
- let cursor = null
203
+ async getCoins({ owner, coinType }) {
204
+ const objects = await this.#getOwnedObjects({ owner, type: `${COIN_TYPE}<${coinType}>` })
205
+ return objects.map(toCoin)
206
+ }
148
207
 
149
- while (true) {
150
- const response = await requestByCursor({ cursor })
151
- coins.push(...response.data)
152
- if (!response.nextCursor || !response.hasNextPage) {
153
- break
154
- }
208
+ async getObjectRefs({ ids }) {
209
+ const data = await this.#query(MULTI_GET_OBJECTS, { keys: ids.map((address) => ({ address })) })
210
+ return data.multiGetObjects.map((object) => object && toObjectRef(object))
211
+ }
212
+
213
+ async #getEpoch() {
214
+ const cached = this.#epoch
215
+ if (cached && SynchronizedTime.now() < cached.startMs + cached.durationMs) return cached
155
216
 
156
- cursor = response.nextCursor
217
+ const { epoch } = await this.#query(LATEST_EPOCH)
218
+ this.#epoch = {
219
+ epochId: Number(epoch.epochId),
220
+ referenceGasPrice: String(epoch.referenceGasPrice),
221
+ startMs: Number(epoch.systemState.startMs.json),
222
+ durationMs: Number(epoch.systemState.durationMs.json),
157
223
  }
224
+ return this.#epoch
225
+ }
158
226
 
159
- return coins
227
+ async getReferenceGasPrice() {
228
+ const { referenceGasPrice } = await this.#getEpoch()
229
+ return referenceGasPrice
160
230
  }
161
231
 
162
- async getCoins({ owner, coinType }) {
163
- const requestByCursor = async ({ cursor }) => {
164
- return this.client.getCoins({
165
- owner,
166
- coinType,
167
- cursor,
168
- limit: 100,
169
- })
232
+ async getLatestSuiSystemState() {
233
+ const { epochId, startMs, durationMs, referenceGasPrice } = await this.#getEpoch()
234
+ return {
235
+ epoch: String(epochId),
236
+ epochStartTimestampMs: String(startMs),
237
+ epochDurationMs: String(durationMs),
238
+ referenceGasPrice,
170
239
  }
240
+ }
241
+
242
+ async #getActivePools() {
243
+ const { epochId } = await this.#getEpoch()
244
+ if (this.#activePools?.epoch !== epochId) this.#activePools = await this.#fetchActivePools()
245
+ return this.#activePools
246
+ }
171
247
 
172
- const coins = []
248
+ async #fetchActivePools() {
249
+ const pools = []
173
250
  let cursor = null
251
+ let epoch
252
+ let inactivePoolsId
174
253
  while (true) {
175
- const response = await requestByCursor({ cursor })
176
- coins.push(...response.data)
177
- if (!response.nextCursor || !response.hasNextPage) {
178
- break
179
- }
254
+ const data = await this.#query(ACTIVE_VALIDATOR_POOLS, { cursor })
255
+ const { validatorSet } = data.epoch
256
+ epoch = data.epoch.epochId
257
+ inactivePoolsId = validatorSet.inactivePoolsId.extract.json
258
+ const { nodes, pageInfo } = validatorSet.activeValidators
259
+ pools.push(...nodes.map(({ contents }) => contents))
260
+ if (!pageInfo.hasNextPage) break
261
+ cursor = pageInfo.endCursor
262
+ }
263
+
264
+ return { epoch: Number(epoch), inactivePoolsId, pools }
265
+ }
180
266
 
181
- cursor = response.nextCursor
267
+ async #getInactivePool({ inactivePoolsId, poolId }) {
268
+ if (!this.#inactivePools.has(poolId)) {
269
+ const contents = await this.#fetchInactivePool({ inactivePoolsId, poolId })
270
+ if (!contents) return
271
+ this.#inactivePools.set(poolId, contents)
182
272
  }
183
273
 
184
- return coins
274
+ return this.#inactivePools.get(poolId)
185
275
  }
186
276
 
187
- async getStakes({ owner }) {
188
- return this.client.getStakes({ owner })
277
+ async #fetchInactivePool({ inactivePoolsId, poolId }) {
278
+ const wrapperData = await this.#query(INACTIVE_VALIDATOR_WRAPPER, {
279
+ table: inactivePoolsId,
280
+ poolId: toBase64(fromHex(poolId)),
281
+ })
282
+ const versioned = wrapperData.address?.dynamicField?.value?.json?.inner
283
+ if (!versioned) return
284
+
285
+ const validatorData = await this.#query(VERSIONED_VALIDATOR, {
286
+ versioned: versioned.id,
287
+ version: `${versioned.version}u64`,
288
+ })
289
+ return validatorData.address?.dynamicField?.value
189
290
  }
190
291
 
191
- async getLatestSuiSystemState() {
192
- return this.client.getLatestSuiSystemState()
292
+ async #getExchangeRates({ table, epochs, currentEpoch }) {
293
+ const cached = this.#exchangeRates.get(table) ?? new Map()
294
+ this.#exchangeRates.set(table, cached)
295
+ const missing = epochs.filter((epoch) => !cached.has(epoch))
296
+ const fetched = await this.#fetchExchangeRates({ table, epochs: missing })
297
+ for (const epoch of missing) {
298
+ const rate = fetched.get(epoch)
299
+ if (rate || epoch < currentEpoch - 1) cached.set(epoch, rate ?? null)
300
+ }
301
+
302
+ return new Map(
303
+ epochs.filter((epoch) => cached.get(epoch)).map((epoch) => [epoch, cached.get(epoch)])
304
+ )
305
+ }
306
+
307
+ async #fetchExchangeRates({ table, epochs }) {
308
+ const results = await Promise.all(
309
+ chunk(epochs, MAX_KEYS_PER_QUERY).map((epochChunk) =>
310
+ this.#query(EXCHANGE_RATES, {
311
+ table,
312
+ keys: epochChunk.map((epoch) => ({ literal: `${epoch}u64` })),
313
+ })
314
+ )
315
+ )
316
+ const fields = results.flatMap((data) => data.address?.multiGetDynamicFields ?? [])
317
+ return new Map(fields.filter(Boolean).map(({ name, value }) => [Number(name.json), value.json]))
318
+ }
319
+
320
+ async #toPool({ contents, stakedSuis, epoch }) {
321
+ const stakingPool = contents.stakingPool.json
322
+ const deactivationEpoch =
323
+ stakingPool.deactivation_epoch === null ? null : Number(stakingPool.deactivation_epoch)
324
+ const latestEpoch = getLatestRateEpoch({ pool: { deactivationEpoch }, epoch })
325
+ const activationEpochs = lodash.uniq(
326
+ stakedSuis
327
+ .filter((stakedSui) => stakedSui.pool_id === stakingPool.id)
328
+ .map((stakedSui) => Number(stakedSui.stake_activation_epoch))
329
+ .filter((activationEpoch) => activationEpoch <= epoch)
330
+ )
331
+ const recordedRates = await this.#getExchangeRates({
332
+ table: stakingPool.exchange_rates.id,
333
+ epochs: getRateLookupEpochs([latestEpoch, ...activationEpochs]),
334
+ currentEpoch: epoch,
335
+ })
336
+
337
+ return {
338
+ validatorAddress: contents.validatorAddress.json,
339
+ deactivationEpoch,
340
+ rates: new Map(
341
+ activationEpochs.map((activationEpoch) => [
342
+ activationEpoch,
343
+ findRecordedRate({ rates: recordedRates, epoch: activationEpoch }),
344
+ ])
345
+ ),
346
+ currentRate: findRecordedRate({ rates: recordedRates, epoch: latestEpoch }),
347
+ }
348
+ }
349
+
350
+ async getStakes({ owner }) {
351
+ const objects = await this.#getOwnedObjects({ owner, type: STAKED_SUI_TYPE })
352
+ if (objects.length === 0) return []
353
+
354
+ const stakedSuis = objects.map(({ contents }) => contents.json)
355
+ const { epoch, inactivePoolsId, pools } = await this.#getActivePools()
356
+ const poolIds = new Set(stakedSuis.map((stakedSui) => stakedSui.pool_id))
357
+ const contentsByPoolId = new Map(
358
+ pools
359
+ .filter(({ stakingPool }) => poolIds.has(stakingPool.json.id))
360
+ .map((contents) => [contents.stakingPool.json.id, contents])
361
+ )
362
+
363
+ const inactivePools = await Promise.all(
364
+ [...poolIds]
365
+ .filter((poolId) => !contentsByPoolId.has(poolId))
366
+ .map(async (poolId) => [poolId, await this.#getInactivePool({ inactivePoolsId, poolId })])
367
+ )
368
+ for (const [poolId, contents] of inactivePools) {
369
+ if (contents) contentsByPoolId.set(poolId, contents)
370
+ }
371
+
372
+ const entries = await Promise.all(
373
+ [...contentsByPoolId].map(async ([poolId, contents]) => [
374
+ poolId,
375
+ await this.#toPool({ contents, stakedSuis, epoch }),
376
+ ])
377
+ )
378
+
379
+ return toDelegatedStakes({ stakedSuis, poolsById: new Map(entries), epoch })
193
380
  }
194
381
 
195
382
  async buildTransaction({ transaction, onlyTransactionKind = false }) {
196
- return transaction.build({ client: this.client, onlyTransactionKind })
383
+ return transaction.build({ client: this, onlyTransactionKind })
384
+ }
385
+
386
+ async #simulate({ transactionDataBcs, checksEnabled }) {
387
+ const data = await this.#query(SIMULATE_TRANSACTION, {
388
+ transaction: { bcs: { value: transactionDataBcs } },
389
+ checksEnabled,
390
+ })
391
+ const effectsJson = data.simulateTransaction.effects?.effectsJson
392
+ if (!effectsJson) throw new Error('Bad sui simulation response: no effects')
393
+ return { effects: toSimulationEffects(effectsJson) }
394
+ }
395
+
396
+ async dryRunTransaction({ transactionBlock }) {
397
+ return this.#simulate({ transactionDataBcs: toBase64(transactionBlock), checksEnabled: true })
197
398
  }
198
399
 
199
400
  async devInspectTransaction({ sender, transactionBlock }) {
200
- return this.client.devInspectTransactionBlock({ sender, transactionBlock })
401
+ const transactionDataBcs = bcs.TransactionData.serialize({
402
+ V1: {
403
+ kind: bcs.TransactionKind.parse(transactionBlock),
404
+ sender,
405
+ gasData: {
406
+ payment: [],
407
+ owner: sender,
408
+ price: await this.getReferenceGasPrice(),
409
+ budget: DEV_INSPECT_GAS_BUDGET,
410
+ },
411
+ expiration: { None: true },
412
+ },
413
+ }).toBase64()
414
+ return this.#simulate({ transactionDataBcs, checksEnabled: false })
415
+ }
416
+
417
+ async #execute({ plainTx, digest }) {
418
+ try {
419
+ const data = await this.#query(
420
+ EXECUTE_TRANSACTION,
421
+ { transactionDataBcs: plainTx.bytes, signatures: [plainTx.signature] },
422
+ { timeoutMs: EXECUTE_TIMEOUT_MS }
423
+ )
424
+ return data.executeTransaction.effects
425
+ } catch (error) {
426
+ if (!(error instanceof GraphQLTimeoutError || error instanceof GraphQLTransportError)) {
427
+ throw error
428
+ }
429
+
430
+ throw new SuiBroadcastError({
431
+ message: 'Sui execution did not complete, the transaction may still execute',
432
+ digest,
433
+ outcome: 'unknown',
434
+ cause: error,
435
+ })
436
+ }
201
437
  }
202
438
 
203
439
  async broadcastTx(plainTx) {
204
- return this.client.executeTransactionBlock({
205
- transactionBlock: plainTx.bytes,
206
- signature: plainTx.signature,
207
- options: {
208
- showInput: true,
209
- showEffects: true,
210
- showObjectChanges: true,
211
- showBalanceChanges: true,
212
- },
213
- })
440
+ const digest = TransactionDataBuilder.getDigestFromBytes(fromBase64(plainTx.bytes))
441
+ const effects = await this.#execute({ plainTx, digest })
442
+ if (!effects?.transaction) {
443
+ throw new SuiBroadcastError({
444
+ message: 'Bad sui execution response: no effects',
445
+ digest,
446
+ outcome: 'unknown',
447
+ })
448
+ }
449
+
450
+ try {
451
+ const [result] = await this.#toTransactionBlockResponses(
452
+ [{ digest, transactionJson: effects.transaction.transactionJson, effects }],
453
+ { tolerateLookupErrors: true }
454
+ )
455
+ return result
456
+ } catch (error) {
457
+ throw new SuiBroadcastError({
458
+ message: 'Sui transaction executed but its response could not be mapped',
459
+ digest,
460
+ outcome: 'executed',
461
+ cause: error,
462
+ })
463
+ }
214
464
  }
215
465
  }