@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 +18 -0
- package/package.json +4 -8
- package/src/api.js +401 -151
- package/src/broadcast-error.js +8 -0
- package/src/create-unsigned-tx.js +2 -2
- package/src/get-fee-async.js +1 -1
- package/src/graphql/graphql-client.js +82 -0
- package/src/graphql/json-rpc-shapes.js +342 -0
- package/src/graphql/queries.js +166 -0
- package/src/index.js +1 -0
- package/src/staking/delegated-stakes.js +66 -0
- package/src/staking/staking-transactions.js +10 -1
- package/src/tx-send.js +51 -2
- package/src/vendors/transactions/resolve.js +2 -3
- package/src/vendors/{experimental/transports/json-rpc-resolver.js → transactions/resolver.js} +18 -56
- package/src/vendors/client/client.js +0 -680
- package/src/vendors/client/errors.js +0 -38
- package/src/vendors/client/http-transport.js +0 -71
- package/src/vendors/client/index.js +0 -1
- package/src/vendors/experimental/cache.js +0 -90
- package/src/vendors/experimental/client.js +0 -27
- package/src/vendors/experimental/core.js +0 -11
- package/src/vendors/experimental/transports/jsonRPC.js +0 -106
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
|
+
"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/
|
|
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": "
|
|
46
|
+
"gitHead": "f210c7a3aa55561320c43aa8d8d5804b49f390d0"
|
|
51
47
|
}
|
package/src/api.js
CHANGED
|
@@ -1,215 +1,465 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
37
|
-
|
|
38
|
-
})
|
|
72
|
+
const separator = this.rpcUrl.endsWith('/') ? '' : '/'
|
|
73
|
+
this.graphql = new GraphQLClient({ url: `${this.rpcUrl}${separator}graphql` })
|
|
39
74
|
}
|
|
40
75
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
const response = await fetchJson(url, fetchOptions)
|
|
63
|
-
return this.handleJsonRPCResponse(response)
|
|
115
|
+
return this.#moveFunctionParameters.get(key)
|
|
64
116
|
}
|
|
65
117
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
126
|
-
return this
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
+
async getReferenceGasPrice() {
|
|
228
|
+
const { referenceGasPrice } = await this.#getEpoch()
|
|
229
|
+
return referenceGasPrice
|
|
160
230
|
}
|
|
161
231
|
|
|
162
|
-
async
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
248
|
+
async #fetchActivePools() {
|
|
249
|
+
const pools = []
|
|
173
250
|
let cursor = null
|
|
251
|
+
let epoch
|
|
252
|
+
let inactivePoolsId
|
|
174
253
|
while (true) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
274
|
+
return this.#inactivePools.get(poolId)
|
|
185
275
|
}
|
|
186
276
|
|
|
187
|
-
async
|
|
188
|
-
|
|
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
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
}
|