@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.
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.1](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.4.0...@exodus/sui-api@1.4.1) (2026-11-02)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **sui-api:** count sui address balances in balances ([#9140](https://github.com/ExodusMovement/assets/issues/9140)) ([9486138](https://github.com/ExodusMovement/assets/commit/9486138e6cedc7edf88f136bdfc998d735756ece))
12
+
13
+
14
+
15
+ ## [1.4.0](https://github.com/ExodusMovement/assets/compare/@exodus/sui-api@1.3.1...@exodus/sui-api@1.4.0) (2026-10-22)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **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))
21
+
22
+
23
+
6
24
  ## [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
25
 
8
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/sui-api",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
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,22 +22,18 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@exodus/asset-lib": "^5.0.0",
25
- "@exodus/assets": "^11.1.2",
26
25
  "@exodus/basic-utils": "^5.1.0",
27
26
  "@exodus/crypto": "^1.0.0-rc.26",
28
- "@exodus/currency": "^6.0.1",
29
27
  "@exodus/models": "^13.0.0",
30
28
  "@exodus/sui-lib": "^1.3.0",
31
29
  "@exodus/sui-meta": "^1.2.0",
32
- "bn.js": "^5.2.1",
33
30
  "lodash": "^4.17.21",
34
31
  "minimalistic-assert": "^1.0.1",
35
- "ms": "^2.1.3",
36
- "url-join": "^4.0.0"
32
+ "ms": "^2.1.3"
37
33
  },
38
34
  "devDependencies": {
39
- "@exodus/asset": "^2.3.0",
40
- "@exodus/assets-testing": "^1.0.0"
35
+ "@exodus/assets-testing": "^1.0.0",
36
+ "@exodus/currency": "^6.0.1"
41
37
  },
42
38
  "homepage": "https://github.com/ExodusMovement/assets/tree/master/sui/sui-api",
43
39
  "bugs": {
@@ -47,5 +43,5 @@
47
43
  "type": "git",
48
44
  "url": "git+https://github.com/ExodusMovement/assets.git"
49
45
  },
50
- "gitHead": "41e6c1bb6a3d919f08cc3ee38068159259b335be"
46
+ "gitHead": "623851699b6a7b3edeedd9d6a3a95aadf1413a31"
51
47
  }
package/src/api.js CHANGED
@@ -1,215 +1,466 @@
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, addressBalance }) => ({
185
+ coinType: toJsonRpcTypeTag(coinType.repr),
186
+ totalBalance,
187
+ addressBalance,
188
+ }))
123
189
  }
124
190
 
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
- },
191
+ async #getOwnedObjects({ owner, type }) {
192
+ return this.#paginate({
193
+ query: OWNED_OBJECTS,
194
+ variables: { owner, type },
195
+ getConnection: (data) => data.address?.objects,
134
196
  })
135
197
  }
136
198
 
137
199
  async getAllCoins({ owner }) {
138
- const requestByCursor = async ({ cursor }) => {
139
- return this.client.getAllCoins({
140
- owner,
141
- cursor,
142
- limit: 100,
143
- })
144
- }
200
+ const objects = await this.#getOwnedObjects({ owner, type: COIN_TYPE })
201
+ return objects.map(toCoin)
202
+ }
145
203
 
146
- const coins = []
147
- let cursor = null
204
+ async getCoins({ owner, coinType }) {
205
+ const objects = await this.#getOwnedObjects({ owner, type: `${COIN_TYPE}<${coinType}>` })
206
+ return objects.map(toCoin)
207
+ }
148
208
 
149
- while (true) {
150
- const response = await requestByCursor({ cursor })
151
- coins.push(...response.data)
152
- if (!response.nextCursor || !response.hasNextPage) {
153
- break
154
- }
209
+ async getObjectRefs({ ids }) {
210
+ const data = await this.#query(MULTI_GET_OBJECTS, { keys: ids.map((address) => ({ address })) })
211
+ return data.multiGetObjects.map((object) => object && toObjectRef(object))
212
+ }
213
+
214
+ async #getEpoch() {
215
+ const cached = this.#epoch
216
+ if (cached && SynchronizedTime.now() < cached.startMs + cached.durationMs) return cached
155
217
 
156
- cursor = response.nextCursor
218
+ const { epoch } = await this.#query(LATEST_EPOCH)
219
+ this.#epoch = {
220
+ epochId: Number(epoch.epochId),
221
+ referenceGasPrice: String(epoch.referenceGasPrice),
222
+ startMs: Number(epoch.systemState.startMs.json),
223
+ durationMs: Number(epoch.systemState.durationMs.json),
157
224
  }
225
+ return this.#epoch
226
+ }
158
227
 
159
- return coins
228
+ async getReferenceGasPrice() {
229
+ const { referenceGasPrice } = await this.#getEpoch()
230
+ return referenceGasPrice
160
231
  }
161
232
 
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
- })
233
+ async getLatestSuiSystemState() {
234
+ const { epochId, startMs, durationMs, referenceGasPrice } = await this.#getEpoch()
235
+ return {
236
+ epoch: String(epochId),
237
+ epochStartTimestampMs: String(startMs),
238
+ epochDurationMs: String(durationMs),
239
+ referenceGasPrice,
170
240
  }
241
+ }
242
+
243
+ async #getActivePools() {
244
+ const { epochId } = await this.#getEpoch()
245
+ if (this.#activePools?.epoch !== epochId) this.#activePools = await this.#fetchActivePools()
246
+ return this.#activePools
247
+ }
171
248
 
172
- const coins = []
249
+ async #fetchActivePools() {
250
+ const pools = []
173
251
  let cursor = null
252
+ let epoch
253
+ let inactivePoolsId
174
254
  while (true) {
175
- const response = await requestByCursor({ cursor })
176
- coins.push(...response.data)
177
- if (!response.nextCursor || !response.hasNextPage) {
178
- break
179
- }
255
+ const data = await this.#query(ACTIVE_VALIDATOR_POOLS, { cursor })
256
+ const { validatorSet } = data.epoch
257
+ epoch = data.epoch.epochId
258
+ inactivePoolsId = validatorSet.inactivePoolsId.extract.json
259
+ const { nodes, pageInfo } = validatorSet.activeValidators
260
+ pools.push(...nodes.map(({ contents }) => contents))
261
+ if (!pageInfo.hasNextPage) break
262
+ cursor = pageInfo.endCursor
263
+ }
264
+
265
+ return { epoch: Number(epoch), inactivePoolsId, pools }
266
+ }
180
267
 
181
- cursor = response.nextCursor
268
+ async #getInactivePool({ inactivePoolsId, poolId }) {
269
+ if (!this.#inactivePools.has(poolId)) {
270
+ const contents = await this.#fetchInactivePool({ inactivePoolsId, poolId })
271
+ if (!contents) return
272
+ this.#inactivePools.set(poolId, contents)
182
273
  }
183
274
 
184
- return coins
275
+ return this.#inactivePools.get(poolId)
185
276
  }
186
277
 
187
- async getStakes({ owner }) {
188
- return this.client.getStakes({ owner })
278
+ async #fetchInactivePool({ inactivePoolsId, poolId }) {
279
+ const wrapperData = await this.#query(INACTIVE_VALIDATOR_WRAPPER, {
280
+ table: inactivePoolsId,
281
+ poolId: toBase64(fromHex(poolId)),
282
+ })
283
+ const versioned = wrapperData.address?.dynamicField?.value?.json?.inner
284
+ if (!versioned) return
285
+
286
+ const validatorData = await this.#query(VERSIONED_VALIDATOR, {
287
+ versioned: versioned.id,
288
+ version: `${versioned.version}u64`,
289
+ })
290
+ return validatorData.address?.dynamicField?.value
189
291
  }
190
292
 
191
- async getLatestSuiSystemState() {
192
- return this.client.getLatestSuiSystemState()
293
+ async #getExchangeRates({ table, epochs, currentEpoch }) {
294
+ const cached = this.#exchangeRates.get(table) ?? new Map()
295
+ this.#exchangeRates.set(table, cached)
296
+ const missing = epochs.filter((epoch) => !cached.has(epoch))
297
+ const fetched = await this.#fetchExchangeRates({ table, epochs: missing })
298
+ for (const epoch of missing) {
299
+ const rate = fetched.get(epoch)
300
+ if (rate || epoch < currentEpoch - 1) cached.set(epoch, rate ?? null)
301
+ }
302
+
303
+ return new Map(
304
+ epochs.filter((epoch) => cached.get(epoch)).map((epoch) => [epoch, cached.get(epoch)])
305
+ )
306
+ }
307
+
308
+ async #fetchExchangeRates({ table, epochs }) {
309
+ const results = await Promise.all(
310
+ chunk(epochs, MAX_KEYS_PER_QUERY).map((epochChunk) =>
311
+ this.#query(EXCHANGE_RATES, {
312
+ table,
313
+ keys: epochChunk.map((epoch) => ({ literal: `${epoch}u64` })),
314
+ })
315
+ )
316
+ )
317
+ const fields = results.flatMap((data) => data.address?.multiGetDynamicFields ?? [])
318
+ return new Map(fields.filter(Boolean).map(({ name, value }) => [Number(name.json), value.json]))
319
+ }
320
+
321
+ async #toPool({ contents, stakedSuis, epoch }) {
322
+ const stakingPool = contents.stakingPool.json
323
+ const deactivationEpoch =
324
+ stakingPool.deactivation_epoch === null ? null : Number(stakingPool.deactivation_epoch)
325
+ const latestEpoch = getLatestRateEpoch({ pool: { deactivationEpoch }, epoch })
326
+ const activationEpochs = lodash.uniq(
327
+ stakedSuis
328
+ .filter((stakedSui) => stakedSui.pool_id === stakingPool.id)
329
+ .map((stakedSui) => Number(stakedSui.stake_activation_epoch))
330
+ .filter((activationEpoch) => activationEpoch <= epoch)
331
+ )
332
+ const recordedRates = await this.#getExchangeRates({
333
+ table: stakingPool.exchange_rates.id,
334
+ epochs: getRateLookupEpochs([latestEpoch, ...activationEpochs]),
335
+ currentEpoch: epoch,
336
+ })
337
+
338
+ return {
339
+ validatorAddress: contents.validatorAddress.json,
340
+ deactivationEpoch,
341
+ rates: new Map(
342
+ activationEpochs.map((activationEpoch) => [
343
+ activationEpoch,
344
+ findRecordedRate({ rates: recordedRates, epoch: activationEpoch }),
345
+ ])
346
+ ),
347
+ currentRate: findRecordedRate({ rates: recordedRates, epoch: latestEpoch }),
348
+ }
349
+ }
350
+
351
+ async getStakes({ owner }) {
352
+ const objects = await this.#getOwnedObjects({ owner, type: STAKED_SUI_TYPE })
353
+ if (objects.length === 0) return []
354
+
355
+ const stakedSuis = objects.map(({ contents }) => contents.json)
356
+ const { epoch, inactivePoolsId, pools } = await this.#getActivePools()
357
+ const poolIds = new Set(stakedSuis.map((stakedSui) => stakedSui.pool_id))
358
+ const contentsByPoolId = new Map(
359
+ pools
360
+ .filter(({ stakingPool }) => poolIds.has(stakingPool.json.id))
361
+ .map((contents) => [contents.stakingPool.json.id, contents])
362
+ )
363
+
364
+ const inactivePools = await Promise.all(
365
+ [...poolIds]
366
+ .filter((poolId) => !contentsByPoolId.has(poolId))
367
+ .map(async (poolId) => [poolId, await this.#getInactivePool({ inactivePoolsId, poolId })])
368
+ )
369
+ for (const [poolId, contents] of inactivePools) {
370
+ if (contents) contentsByPoolId.set(poolId, contents)
371
+ }
372
+
373
+ const entries = await Promise.all(
374
+ [...contentsByPoolId].map(async ([poolId, contents]) => [
375
+ poolId,
376
+ await this.#toPool({ contents, stakedSuis, epoch }),
377
+ ])
378
+ )
379
+
380
+ return toDelegatedStakes({ stakedSuis, poolsById: new Map(entries), epoch })
193
381
  }
194
382
 
195
383
  async buildTransaction({ transaction, onlyTransactionKind = false }) {
196
- return transaction.build({ client: this.client, onlyTransactionKind })
384
+ return transaction.build({ client: this, onlyTransactionKind })
385
+ }
386
+
387
+ async #simulate({ transactionDataBcs, checksEnabled }) {
388
+ const data = await this.#query(SIMULATE_TRANSACTION, {
389
+ transaction: { bcs: { value: transactionDataBcs } },
390
+ checksEnabled,
391
+ })
392
+ const effectsJson = data.simulateTransaction.effects?.effectsJson
393
+ if (!effectsJson) throw new Error('Bad sui simulation response: no effects')
394
+ return { effects: toSimulationEffects(effectsJson) }
395
+ }
396
+
397
+ async dryRunTransaction({ transactionBlock }) {
398
+ return this.#simulate({ transactionDataBcs: toBase64(transactionBlock), checksEnabled: true })
197
399
  }
198
400
 
199
401
  async devInspectTransaction({ sender, transactionBlock }) {
200
- return this.client.devInspectTransactionBlock({ sender, transactionBlock })
402
+ const transactionDataBcs = bcs.TransactionData.serialize({
403
+ V1: {
404
+ kind: bcs.TransactionKind.parse(transactionBlock),
405
+ sender,
406
+ gasData: {
407
+ payment: [],
408
+ owner: sender,
409
+ price: await this.getReferenceGasPrice(),
410
+ budget: DEV_INSPECT_GAS_BUDGET,
411
+ },
412
+ expiration: { None: true },
413
+ },
414
+ }).toBase64()
415
+ return this.#simulate({ transactionDataBcs, checksEnabled: false })
416
+ }
417
+
418
+ async #execute({ plainTx, digest }) {
419
+ try {
420
+ const data = await this.#query(
421
+ EXECUTE_TRANSACTION,
422
+ { transactionDataBcs: plainTx.bytes, signatures: [plainTx.signature] },
423
+ { timeoutMs: EXECUTE_TIMEOUT_MS }
424
+ )
425
+ return data.executeTransaction.effects
426
+ } catch (error) {
427
+ if (!(error instanceof GraphQLTimeoutError || error instanceof GraphQLTransportError)) {
428
+ throw error
429
+ }
430
+
431
+ throw new SuiBroadcastError({
432
+ message: 'Sui execution did not complete, the transaction may still execute',
433
+ digest,
434
+ outcome: 'unknown',
435
+ cause: error,
436
+ })
437
+ }
201
438
  }
202
439
 
203
440
  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
- })
441
+ const digest = TransactionDataBuilder.getDigestFromBytes(fromBase64(plainTx.bytes))
442
+ const effects = await this.#execute({ plainTx, digest })
443
+ if (!effects?.transaction) {
444
+ throw new SuiBroadcastError({
445
+ message: 'Bad sui execution response: no effects',
446
+ digest,
447
+ outcome: 'unknown',
448
+ })
449
+ }
450
+
451
+ try {
452
+ const [result] = await this.#toTransactionBlockResponses(
453
+ [{ digest, transactionJson: effects.transaction.transactionJson, effects }],
454
+ { tolerateLookupErrors: true }
455
+ )
456
+ return result
457
+ } catch (error) {
458
+ throw new SuiBroadcastError({
459
+ message: 'Sui transaction executed but its response could not be mapped',
460
+ digest,
461
+ outcome: 'executed',
462
+ cause: error,
463
+ })
464
+ }
214
465
  }
215
466
  }