@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 +18 -0
- package/package.json +5 -9
- package/src/api.js +402 -151
- package/src/balances.js +10 -10
- package/src/basic-account-state.js +1 -0
- 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/sui-no-history-monitor.js +32 -19
- 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.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
|
+
"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/
|
|
40
|
-
"@exodus/
|
|
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": "
|
|
46
|
+
"gitHead": "623851699b6a7b3edeedd9d6a3a95aadf1413a31"
|
|
51
47
|
}
|
package/src/api.js
CHANGED
|
@@ -1,215 +1,466 @@
|
|
|
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, addressBalance }) => ({
|
|
185
|
+
coinType: toJsonRpcTypeTag(coinType.repr),
|
|
186
|
+
totalBalance,
|
|
187
|
+
addressBalance,
|
|
188
|
+
}))
|
|
123
189
|
}
|
|
124
190
|
|
|
125
|
-
async
|
|
126
|
-
return this
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
+
async getReferenceGasPrice() {
|
|
229
|
+
const { referenceGasPrice } = await this.#getEpoch()
|
|
230
|
+
return referenceGasPrice
|
|
160
231
|
}
|
|
161
232
|
|
|
162
|
-
async
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
249
|
+
async #fetchActivePools() {
|
|
250
|
+
const pools = []
|
|
173
251
|
let cursor = null
|
|
252
|
+
let epoch
|
|
253
|
+
let inactivePoolsId
|
|
174
254
|
while (true) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
|
275
|
+
return this.#inactivePools.get(poolId)
|
|
185
276
|
}
|
|
186
277
|
|
|
187
|
-
async
|
|
188
|
-
|
|
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
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
}
|