@mysten/sui 2.28.0 → 2.30.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 +27 -0
- package/dist/client/core-resolver.d.mts.map +1 -1
- package/dist/client/core-resolver.mjs +2 -5
- package/dist/client/core-resolver.mjs.map +1 -1
- package/dist/cryptography/signature.d.mts +14 -14
- package/dist/grpc/client.d.mts +1 -1
- package/dist/grpc/client.d.mts.map +1 -1
- package/dist/grpc/client.mjs +3 -5
- package/dist/grpc/client.mjs.map +1 -1
- package/dist/grpc/core.d.mts.map +1 -1
- package/dist/grpc/core.mjs +10 -10
- package/dist/grpc/core.mjs.map +1 -1
- package/dist/grpc/index.d.mts +4 -3
- package/dist/grpc/index.mjs +4 -2
- package/dist/grpc/proto/sui/forking/v1alpha/forking_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/move_package_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/name_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/signature_verification_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/state_service.client.d.mts +4 -4
- package/dist/grpc/proto/sui/rpc/v2/transaction_execution_service.client.d.mts +4 -4
- package/dist/grpc/transport.d.mts +18 -0
- package/dist/grpc/transport.d.mts.map +1 -0
- package/dist/grpc/transport.mjs +97 -0
- package/dist/grpc/transport.mjs.map +1 -0
- package/dist/transactions/Transaction.d.mts +7 -10
- package/dist/transactions/Transaction.d.mts.map +1 -1
- package/dist/transactions/Transaction.mjs.map +1 -1
- package/dist/transactions/data/internal.d.mts +127 -127
- package/dist/transactions/data/v1.d.mts +239 -239
- package/dist/transactions/data/v1.d.mts.map +1 -1
- package/dist/transactions/data/v2.d.mts +16 -16
- package/dist/transactions/data/v2.d.mts.map +1 -1
- package/dist/transactions/intents/CoinWithBalance.mjs +4 -2
- package/dist/transactions/intents/CoinWithBalance.mjs.map +1 -1
- package/dist/transactions/resolution-utils.mjs +13 -0
- package/dist/transactions/resolution-utils.mjs.map +1 -0
- package/dist/transactions/resolve.d.mts +8 -0
- package/dist/transactions/resolve.d.mts.map +1 -1
- package/dist/transactions/resolve.mjs +6 -2
- package/dist/transactions/resolve.mjs.map +1 -1
- package/dist/version.mjs +1 -1
- package/dist/version.mjs.map +1 -1
- package/dist/zklogin/bcs.d.mts +14 -14
- package/docs/clients/grpc.md +31 -7
- package/docs/migrations/sui-2.0/json-rpc-migration.md +50 -1
- package/docs/transactions/basics.md +19 -14
- package/docs/transactions/offline.md +137 -88
- package/package.json +2 -2
- package/src/client/core-resolver.ts +2 -7
- package/src/grpc/client.ts +12 -4
- package/src/grpc/core.ts +18 -14
- package/src/grpc/index.ts +7 -3
- package/src/grpc/transport.ts +160 -0
- package/src/transactions/Transaction.ts +1 -4
- package/src/transactions/intents/CoinWithBalance.ts +32 -25
- package/src/transactions/resolution-utils.ts +18 -0
- package/src/transactions/resolve.ts +29 -2
- package/src/version.ts +1 -1
|
@@ -2,69 +2,132 @@
|
|
|
2
2
|
|
|
3
3
|
> Build transactions without a network connection
|
|
4
4
|
|
|
5
|
-
Normally the
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Normally `build()` queries the network to resolve object versions, resolve intents like `tx.coin()`,
|
|
6
|
+
fetch the gas price, estimate the gas budget, pick gas coins, and set an expiration when gas is paid
|
|
7
|
+
from address balance. To build without a client, provide that information yourself. See also the Sui
|
|
8
|
+
documentation on
|
|
8
9
|
[offline signing](https://docs.sui.io/guides/developer/transactions/transaction-auth/offline-signing)
|
|
9
10
|
for the protocol-level details.
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Building only the transaction kind
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
or `coinWithBalance` because they require a client to resolve coin objects at build time.
|
|
14
|
+
If the next step only needs the inputs and commands (for example, a sponsor or a backend that fills
|
|
15
|
+
in gas), build with `onlyTransactionKind`. Sender, gas data, and expiration are left out:
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
18
|
|
|
19
19
|
const tx = new Transaction();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const [coin] = tx.moveCall({
|
|
24
|
-
target: '0x2::coin::redeem_funds',
|
|
25
|
-
typeArguments: ['0x2::sui::SUI'],
|
|
26
|
-
arguments: [tx.withdrawal({ amount: 1_000_000_000 })],
|
|
20
|
+
tx.moveCall({
|
|
21
|
+
target: '0xPackage::module::function',
|
|
22
|
+
arguments: [tx.pure.u64(100)],
|
|
27
23
|
});
|
|
28
|
-
tx.transferObjects([coin], '0xRecipientAddress');
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
const kindBytes = await tx.build({ onlyTransactionKind: true });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Inputs and intents still need to be resolved. Use full object references, `tx.withdrawal()`, or
|
|
29
|
+
[`assumeSufficientAddressBalances`](#coin-and-balance-intents) so nothing needs a lookup.
|
|
30
|
+
|
|
31
|
+
## Building full transaction bytes
|
|
32
|
+
|
|
33
|
+
A full offline build needs the sender and all gas data:
|
|
34
|
+
|
|
35
|
+
| Method | Description |
|
|
36
|
+
| ----------------- | --------------------------------------------------------------- |
|
|
37
|
+
| `setSender()` | The address executing the transaction |
|
|
38
|
+
| `setGasPrice()` | Reference gas price (query `getReferenceGasPrice()` beforehand) |
|
|
39
|
+
| `setGasBudget()` | Maximum gas to spend (in MIST). Estimating it requires a client |
|
|
40
|
+
| `setGasPayment()` | Coin object references, or `[]` to pay gas from address balance |
|
|
41
|
+
| `setGasOwner()` | Only for sponsored transactions. Defaults to the sender |
|
|
42
|
+
|
|
43
|
+
### Paying gas from address balance
|
|
44
|
+
|
|
45
|
+
With `setGasPayment([])`, gas is paid from the gas owner's SUI address balance. Nothing ties the
|
|
46
|
+
transaction to a specific object version, so it also needs a `ValidDuring` expiration for replay
|
|
47
|
+
protection:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
|
|
51
|
+
// Look these up before going offline
|
|
52
|
+
const referenceGasPrice = 1000n;
|
|
53
|
+
const currentEpoch = 100;
|
|
54
|
+
const chainIdentifier = 'Base58ChainIdentifier'; // from getChainIdentifier()
|
|
55
|
+
|
|
56
|
+
const tx = new Transaction();
|
|
57
|
+
|
|
58
|
+
// FundsWithdrawal inputs contain the amount and type, so no object lookup is needed
|
|
31
59
|
tx.moveCall({
|
|
32
|
-
target: '
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
objectId: '0xSharedObjectId',
|
|
36
|
-
initialSharedVersion: '1',
|
|
37
|
-
mutable: true,
|
|
38
|
-
}),
|
|
39
|
-
],
|
|
60
|
+
target: '0x2::balance::send_funds',
|
|
61
|
+
typeArguments: ['0x2::sui::SUI'],
|
|
62
|
+
arguments: [tx.withdrawal({ amount: 1_000_000_000 }), tx.pure.address('0xRecipientAddress')],
|
|
40
63
|
});
|
|
41
64
|
|
|
42
|
-
// Required configuration for all offline builds
|
|
43
65
|
tx.setSender('0xSenderAddress');
|
|
44
|
-
tx.setGasPrice(
|
|
66
|
+
tx.setGasPrice(referenceGasPrice);
|
|
45
67
|
tx.setGasBudget(50_000_000);
|
|
46
|
-
tx.setGasPayment([]);
|
|
68
|
+
tx.setGasPayment([]);
|
|
47
69
|
|
|
48
|
-
// Expiration is required when there are no owned objects for gas or inputs
|
|
49
70
|
tx.setExpiration({
|
|
50
71
|
ValidDuring: {
|
|
51
|
-
minEpoch:
|
|
52
|
-
maxEpoch:
|
|
72
|
+
minEpoch: currentEpoch,
|
|
73
|
+
maxEpoch: currentEpoch + 1,
|
|
53
74
|
minTimestamp: null,
|
|
54
75
|
maxTimestamp: null,
|
|
55
|
-
chain:
|
|
56
|
-
|
|
76
|
+
chain: chainIdentifier,
|
|
77
|
+
// Must be unique for each transaction in the validity window, including across restarts
|
|
78
|
+
nonce: await nonceStore.next(),
|
|
57
79
|
},
|
|
58
80
|
});
|
|
59
81
|
|
|
60
|
-
// Build without a client
|
|
61
82
|
const bytes = await tx.build();
|
|
62
83
|
```
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
Two otherwise-identical transactions with the same nonce have the same digest, and the second one is
|
|
86
|
+
rejected as a duplicate. Use a counter that survives restarts, not a hard-coded value.
|
|
87
|
+
|
|
88
|
+
The address balance needs to cover the gas budget on top of any withdrawals. Don't add the budget to
|
|
89
|
+
the withdrawal amount.
|
|
90
|
+
|
|
91
|
+
`{ Epoch: n }` expiration does not provide replay protection. Use `ValidDuring`.
|
|
66
92
|
|
|
67
|
-
|
|
93
|
+
### Paying gas with coin objects
|
|
94
|
+
|
|
95
|
+
Gas coins need an exact version and digest. Other SUI can still come from address balance:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
|
|
99
|
+
const tx = new Transaction();
|
|
100
|
+
tx.moveCall({
|
|
101
|
+
target: '0x2::balance::send_funds',
|
|
102
|
+
typeArguments: ['0x2::sui::SUI'],
|
|
103
|
+
arguments: [
|
|
104
|
+
tx.balance({ balance: 1_000_000, useGasCoin: false }),
|
|
105
|
+
tx.pure.address('0xRecipientAddress'),
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
tx.setSender('0xSenderAddress');
|
|
110
|
+
tx.setGasPrice(1000);
|
|
111
|
+
tx.setGasBudget(50_000_000);
|
|
112
|
+
tx.setGasPayment([{ objectId: '0xGasCoinId', version: '3', digest: 'Base58GasCoinDigest' }]);
|
|
113
|
+
|
|
114
|
+
// assumeSufficientAddressBalances resolves tx.balance() without a client (see below)
|
|
115
|
+
const bytes = await tx.build({ assumeSufficientAddressBalances: true });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Object inputs
|
|
119
|
+
|
|
120
|
+
### Shared and party objects
|
|
121
|
+
|
|
122
|
+
Shared objects only need `objectId` and `initialSharedVersion`, both of which are stable:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
tx.sharedObjectRef({
|
|
126
|
+
objectId: '0xSharedObjectId',
|
|
127
|
+
initialSharedVersion: '1',
|
|
128
|
+
mutable: true,
|
|
129
|
+
});
|
|
130
|
+
```
|
|
68
131
|
|
|
69
132
|
Party objects are address-owned but consensus-versioned, with per-address permissions. They are
|
|
70
133
|
referenced the same way as shared objects:
|
|
@@ -83,12 +146,11 @@ Key properties for offline building:
|
|
|
83
146
|
becomes a party object
|
|
84
147
|
- **Enable pipelining**: Submit multiple transactions on the same party object without waiting for
|
|
85
148
|
each one to finalize
|
|
86
|
-
- **Cannot be used for gas**:
|
|
149
|
+
- **Cannot be used for gas**: Pay gas from address balance or with a SUI coin object
|
|
87
150
|
|
|
88
|
-
|
|
151
|
+
### Owned and immutable objects
|
|
89
152
|
|
|
90
|
-
|
|
91
|
-
for each one:
|
|
153
|
+
Owned and immutable objects need the exact version and digest:
|
|
92
154
|
|
|
93
155
|
```typescript
|
|
94
156
|
|
|
@@ -122,71 +184,58 @@ tx.moveCall({
|
|
|
122
184
|
}),
|
|
123
185
|
],
|
|
124
186
|
});
|
|
125
|
-
|
|
126
|
-
// Gas payment with specific coin objects
|
|
127
|
-
tx.setGasPayment([{ objectId: '0xGasCoinId', version: '3', digest: 'jkl012...' }]);
|
|
128
|
-
|
|
129
|
-
tx.setSender('0xSenderAddress');
|
|
130
|
-
tx.setGasPrice(1000);
|
|
131
|
-
tx.setGasBudget(50_000_000);
|
|
132
|
-
|
|
133
|
-
const bytes = await tx.build();
|
|
134
187
|
```
|
|
135
188
|
|
|
136
|
-
##
|
|
189
|
+
## Coin and balance intents
|
|
137
190
|
|
|
138
|
-
|
|
191
|
+
`tx.coin()` and `tx.balance()` normally look up the sender's address balance and coin objects. Pass
|
|
192
|
+
`assumeSufficientAddressBalances` to skip the lookups and withdraw from address balance instead:
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
| ----------------- | --------------------------------------------------------------- |
|
|
142
|
-
| `setSender()` | The address executing the transaction |
|
|
143
|
-
| `setGasPrice()` | Reference gas price (query `getReferenceGasPrice()` beforehand) |
|
|
144
|
-
| `setGasBudget()` | Maximum gas to spend (in MIST) |
|
|
145
|
-
| `setGasPayment()` | Coin object references, or `[]` for address balance |
|
|
194
|
+
```typescript
|
|
146
195
|
|
|
147
|
-
|
|
196
|
+
const tx = new Transaction();
|
|
197
|
+
tx.moveCall({
|
|
198
|
+
target: '0x2::balance::send_funds',
|
|
199
|
+
typeArguments: ['0xPackage::module::TOKEN'],
|
|
200
|
+
arguments: [
|
|
201
|
+
tx.balance({ type: '0xPackage::module::TOKEN', balance: 1_000_000 }),
|
|
202
|
+
tx.pure.address('0xRecipientAddress'),
|
|
203
|
+
],
|
|
204
|
+
});
|
|
148
205
|
|
|
149
|
-
|
|
150
|
-
|
|
206
|
+
// The sender is still required, since the withdrawal comes from its address balance
|
|
207
|
+
tx.setSender('0xSenderAddress');
|
|
151
208
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
minEpoch: 100, // current epoch
|
|
156
|
-
maxEpoch: 101, // typically current epoch + 1
|
|
157
|
-
minTimestamp: null,
|
|
158
|
-
maxTimestamp: null,
|
|
159
|
-
chain: 'mainnet',
|
|
160
|
-
nonce: 0, // increment for multiple transactions in the same epoch
|
|
161
|
-
},
|
|
209
|
+
const kindBytes = await tx.build({
|
|
210
|
+
onlyTransactionKind: true,
|
|
211
|
+
assumeSufficientAddressBalances: true,
|
|
162
212
|
});
|
|
163
213
|
```
|
|
164
214
|
|
|
165
|
-
|
|
215
|
+
> **Warning:** Nothing is checked. The transaction builds, then fails at execution if the address balance doesn't
|
|
216
|
+
> cover it.
|
|
166
217
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
218
|
+
SUI intents withdraw from address balance too, whether or not the transaction also uses `tx.gas`.
|
|
219
|
+
This matches an online build when the address balance is sufficient. Mixing default and
|
|
220
|
+
`useGasCoin: false` SUI intents in one transaction is still an error.
|
|
170
221
|
|
|
171
|
-
|
|
172
|
-
|
|
222
|
+
On a full build, the option also sets an unset gas payment to `[]`, but only when nothing else needs
|
|
223
|
+
a client, the transaction doesn't use `tx.gas`, and a `ValidDuring` or `Validity` expiration is
|
|
224
|
+
already set. Sender, gas price, and gas budget still need to be provided. Once set, the empty
|
|
225
|
+
payment is part of the transaction, the same as calling `setGasPayment([])`.
|
|
173
226
|
|
|
174
227
|
## Serialization
|
|
175
228
|
|
|
176
|
-
|
|
229
|
+
`toJSON()` resolves async thunks and intents but does not fill in gas or object versions. Pass
|
|
230
|
+
`supportedIntents` to keep an intent for another system to resolve:
|
|
177
231
|
|
|
178
232
|
```typescript
|
|
179
|
-
//
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
// Build with a client — only makes network requests when there is unresolved data to look up
|
|
183
|
-
const bytes = await tx.build({ client: grpcClient });
|
|
184
|
-
```
|
|
233
|
+
// Intents are resolved, so tx.coin() needs a client or assumeSufficientAddressBalances
|
|
234
|
+
const json = await tx.toJSON({ assumeSufficientAddressBalances: true });
|
|
235
|
+
const restored = Transaction.from(json);
|
|
185
236
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
```typescript
|
|
189
|
-
const tx = Transaction.from(bytes);
|
|
237
|
+
// Or keep tx.coin() intents for the receiver to resolve
|
|
238
|
+
const jsonWithIntents = await tx.toJSON({ supportedIntents: ['CoinWithBalance'] });
|
|
190
239
|
```
|
|
191
240
|
|
|
192
|
-
|
|
241
|
+
`Transaction.from()` accepts JSON strings, BCS bytes, and base64-encoded BCS.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"author": "Mysten Labs <build@mystenlabs.com>",
|
|
4
4
|
"description": "Sui TypeScript API",
|
|
5
5
|
"homepage": "https://sdk.mystenlabs.com",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.30.0",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"files": [
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"typescript": "^7.0.2",
|
|
152
152
|
"vite": "^8.2.1",
|
|
153
153
|
"vite-tsconfig-paths": "^6.0.4",
|
|
154
|
-
"vitest": "^4.1.
|
|
154
|
+
"vitest": "^4.1.11",
|
|
155
155
|
"wait-on": "^9.1.0"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
@@ -17,6 +17,7 @@ import { getPureBcsSchema, isTxContext } from '../transactions/serializer.js';
|
|
|
17
17
|
import type { TransactionDataBuilder } from '../transactions/TransactionData.js';
|
|
18
18
|
import { chunk } from '@mysten/utils';
|
|
19
19
|
import type { BuildTransactionOptions } from '../transactions/index.js';
|
|
20
|
+
import { transactionUsesGasCoin } from '../transactions/resolution-utils.js';
|
|
20
21
|
|
|
21
22
|
// The maximum objects that can be fetched at once using multiGetObjects.
|
|
22
23
|
const MAX_OBJECTS_PER_FETCH = 50;
|
|
@@ -54,19 +55,13 @@ export async function coreClientResolveTransactionPlugin(
|
|
|
54
55
|
next: () => Promise<void>,
|
|
55
56
|
) {
|
|
56
57
|
const client = getClient(options);
|
|
57
|
-
|
|
58
58
|
const needsGasPrice = !options.onlyTransactionKind && !transactionData.gasData.price;
|
|
59
59
|
const needsPayment = !options.onlyTransactionKind && !transactionData.gasData.payment;
|
|
60
60
|
const gasPayer = transactionData.gasData.owner ?? transactionData.sender;
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const usesGasCoin = transactionUsesGasCoin(transactionData);
|
|
63
63
|
let withdrawals = 0n;
|
|
64
64
|
|
|
65
|
-
transactionData.mapArguments((arg) => {
|
|
66
|
-
if (arg.$kind === 'GasCoin') usesGasCoin = true;
|
|
67
|
-
return arg;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
65
|
const normalizedGasPayer = gasPayer ? normalizeSuiAddress(gasPayer) : null;
|
|
71
66
|
for (const input of transactionData.inputs) {
|
|
72
67
|
if (input.$kind !== 'FundsWithdrawal' || !normalizedGasPayer) continue;
|
package/src/grpc/client.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import type { GrpcWebOptions } from '@protobuf-ts/grpcweb-transport';
|
|
5
|
-
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
|
|
6
5
|
import { TransactionExecutionServiceClient } from './proto/sui/rpc/v2/transaction_execution_service.client.js';
|
|
7
6
|
import { LedgerServiceClient } from './proto/sui/rpc/v2/ledger_service.client.js';
|
|
8
7
|
import { MovePackageServiceClient } from './proto/sui/rpc/v2/move_package_service.client.js';
|
|
@@ -19,6 +18,7 @@ import { fromBase64, toBase64 } from '@mysten/utils';
|
|
|
19
18
|
import { NameServiceClient } from './proto/sui/rpc/v2/name_service.client.js';
|
|
20
19
|
import { ForkingServiceClient } from './proto/sui/forking/v1alpha/forking_service.client.js';
|
|
21
20
|
import type { TransactionPlugin } from '../transactions/index.js';
|
|
21
|
+
import { GrpcWebFetchTransport } from './transport.js';
|
|
22
22
|
|
|
23
23
|
interface SuiGrpcTransportOptions extends GrpcWebOptions {
|
|
24
24
|
transport?: never;
|
|
@@ -157,9 +157,17 @@ export class SuiGrpcClient extends BaseClient implements SuiClientTypes.Transpor
|
|
|
157
157
|
|
|
158
158
|
constructor(options: SuiGrpcClientOptions) {
|
|
159
159
|
super({ network: options.network });
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
const {
|
|
161
|
+
network: _network,
|
|
162
|
+
mvr: _mvr,
|
|
163
|
+
// Not forwarded: every Core API call passes its own `signal`, which would overwrite it.
|
|
164
|
+
abort: _abort,
|
|
165
|
+
transport: providedTransport,
|
|
166
|
+
...transportOptions
|
|
167
|
+
} = options as SuiGrpcClientOptions & SuiGrpcTransportOptions & { transport?: RpcTransport };
|
|
168
|
+
|
|
169
|
+
// A caller-supplied transport is used as given. See ./transport.ts for the default.
|
|
170
|
+
const transport = providedTransport ?? new GrpcWebFetchTransport(transportOptions);
|
|
163
171
|
this.transactionExecutionService = new TransactionExecutionServiceClient(transport);
|
|
164
172
|
this.ledgerService = new LedgerServiceClient(transport);
|
|
165
173
|
this.stateService = new StateServiceClient(transport);
|
package/src/grpc/core.ts
CHANGED
|
@@ -69,24 +69,28 @@ import {
|
|
|
69
69
|
validateTransactionQuery,
|
|
70
70
|
} from '../client/query-filters.js';
|
|
71
71
|
import { toGrpcEventFilter, toGrpcTransactionFilter } from './filters.js';
|
|
72
|
+
import { hasDecodedStatusMessage } from './transport.js';
|
|
72
73
|
|
|
73
74
|
export interface GrpcCoreClientOptions extends CoreClientOptions {
|
|
74
75
|
client: SuiGrpcClient;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
function isNameServiceResolutionMiss(error: unknown): boolean {
|
|
78
|
-
|
|
79
|
-
if (error
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
// Read by shape, so an error from another copy of runtime-rpc is still recognised.
|
|
80
|
+
if (typeof error !== 'object' || error === null) return false;
|
|
81
|
+
|
|
82
|
+
const { code, message } = error as { code?: unknown; message?: unknown };
|
|
83
|
+
if (typeof code !== 'string' || typeof message !== 'string') return false;
|
|
84
|
+
|
|
85
|
+
if (code === GrpcStatusCode[GrpcStatusCode.NOT_FOUND]) return true;
|
|
86
|
+
if (code !== GrpcStatusCode[GrpcStatusCode.RESOURCE_EXHAUSTED]) return false;
|
|
87
|
+
|
|
88
|
+
// The service reports an expired name as RESOURCE_EXHAUSTED with no structured reason, so match
|
|
89
|
+
// the status text and leave other RESOURCE_EXHAUSTED failures alone. A transport this package did
|
|
90
|
+
// not build leaves `grpc-message` encoded, hence the second form.
|
|
91
|
+
if (message === 'name has expired') return true;
|
|
92
|
+
|
|
93
|
+
return !hasDecodedStatusMessage(error) && message === 'name%20has%20expired';
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
export class GrpcCoreClient extends CoreClient {
|
|
@@ -987,9 +991,9 @@ export class GrpcCoreClient extends CoreClient {
|
|
|
987
991
|
});
|
|
988
992
|
response = result.response;
|
|
989
993
|
} catch (error) {
|
|
990
|
-
//
|
|
994
|
+
// The transport owns the status text. See ./transport.ts.
|
|
991
995
|
if (error instanceof Error && error.message) {
|
|
992
|
-
throw new SimulationError(
|
|
996
|
+
throw new SimulationError(error.message, { cause: error });
|
|
993
997
|
}
|
|
994
998
|
throw error;
|
|
995
999
|
}
|
package/src/grpc/index.ts
CHANGED
|
@@ -23,10 +23,14 @@ export type {
|
|
|
23
23
|
} from './client.js';
|
|
24
24
|
export type { GrpcCoreClientOptions } from './core.js';
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
// Subclasses `@protobuf-ts/grpcweb-transport`'s transport to fix its error handling.
|
|
27
|
+
export { GrpcWebFetchTransport } from './transport.js';
|
|
28
|
+
|
|
29
|
+
// Re-exported so users can configure a transport, and narrow and code the errors it produces,
|
|
30
|
+
// without adding @protobuf-ts/* as a dependency.
|
|
31
|
+
export { GrpcStatusCode } from '@protobuf-ts/grpcweb-transport';
|
|
29
32
|
export type { GrpcWebOptions } from '@protobuf-ts/grpcweb-transport';
|
|
33
|
+
export { RpcError } from '@protobuf-ts/runtime-rpc';
|
|
30
34
|
export type { RpcTransport } from '@protobuf-ts/runtime-rpc';
|
|
31
35
|
|
|
32
36
|
// Export all gRPC proto types as a namespace
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { GrpcStatusCode } from '@protobuf-ts/grpcweb-transport';
|
|
5
|
+
import { GrpcWebFetchTransport as UpstreamGrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';
|
|
6
|
+
import type {
|
|
7
|
+
MethodInfo,
|
|
8
|
+
RpcOptions,
|
|
9
|
+
ServerStreamingCall,
|
|
10
|
+
UnaryCall,
|
|
11
|
+
} from '@protobuf-ts/runtime-rpc';
|
|
12
|
+
import { RpcError } from '@protobuf-ts/runtime-rpc';
|
|
13
|
+
|
|
14
|
+
// A failed call rejects four promises with the same error, so it is only decoded once. Registered
|
|
15
|
+
// globally so another installed copy of this package sees the same marker.
|
|
16
|
+
const MESSAGE_DECODED = Symbol.for('@mysten/sui/grpc/decoded-status-message');
|
|
17
|
+
|
|
18
|
+
// `grpc-message` is percent-encoded on the wire and the upstream transport does not decode it:
|
|
19
|
+
// https://github.com/timostamm/protobuf-ts/pull/739
|
|
20
|
+
function decodeGrpcStatusMessage(message: string): string {
|
|
21
|
+
if (!message.includes('%')) return message;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
return decodeURIComponent(message);
|
|
25
|
+
} catch {
|
|
26
|
+
return message;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function markStatusMessageRead(error: RpcError): boolean {
|
|
31
|
+
if (MESSAGE_DECODED in error) return false;
|
|
32
|
+
Object.defineProperty(error, MESSAGE_DECODED, { value: true, configurable: true });
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function decodeStatusMessageOnce(error: RpcError): void {
|
|
38
|
+
if (markStatusMessageRead(error)) error.message = decodeGrpcStatusMessage(error.message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Whether this package's transport decoded the error's status text. */
|
|
42
|
+
export function hasDecodedStatusMessage(error: object): boolean {
|
|
43
|
+
return MESSAGE_DECODED in error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// An abort reason is arbitrary, and reading a property on it runs code the caller wrote. A read
|
|
47
|
+
// that throws counts as absent, so one bad accessor does not decide the status.
|
|
48
|
+
function readProperty(value: unknown, key: 'code' | 'name' | 'message'): unknown {
|
|
49
|
+
if (typeof value !== 'object' || value === null) return undefined;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
return (value as Record<string, unknown>)[key];
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The reason is read by shape, since another copy of runtime-rpc or another realm fails
|
|
59
|
+
// `instanceof`. `AbortSignal.timeout` aborts with a `TimeoutError`.
|
|
60
|
+
function abortStatus(reason: unknown): string {
|
|
61
|
+
const code = readProperty(reason, 'code');
|
|
62
|
+
|
|
63
|
+
// A status name maps to a number; `in` would also match the enum's reverse mapping.
|
|
64
|
+
if (
|
|
65
|
+
typeof code === 'string' &&
|
|
66
|
+
typeof GrpcStatusCode[code as keyof typeof GrpcStatusCode] === 'number'
|
|
67
|
+
) {
|
|
68
|
+
return code;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return readProperty(reason, 'name') === 'TimeoutError'
|
|
72
|
+
? GrpcStatusCode[GrpcStatusCode.DEADLINE_EXCEEDED]
|
|
73
|
+
: GrpcStatusCode[GrpcStatusCode.CANCELLED];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The transport reuses the abort reason's message, so matching it says the text is local. A fetch
|
|
77
|
+
// that substitutes its own error (node-fetch) gives text of its own, which holds nothing to decode.
|
|
78
|
+
// The reason is never coerced: a symbol or a throwing hook would throw here.
|
|
79
|
+
function isAbortReasonText(message: string, reason: unknown): boolean {
|
|
80
|
+
if (typeof reason === 'string') return message === reason;
|
|
81
|
+
|
|
82
|
+
const reasonMessage = readProperty(reason, 'message');
|
|
83
|
+
|
|
84
|
+
return typeof reasonMessage === 'string' && message === reasonMessage;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Rewritten in place, so every promise a call rejects surfaces the same error. Only the two codes
|
|
88
|
+
// upstream uses for an abort are re-coded; a failure that raced the abort is relabelled with it.
|
|
89
|
+
function normalizeGrpcError(error: unknown, signal: AbortSignal | undefined): void {
|
|
90
|
+
try {
|
|
91
|
+
normalize(error, signal);
|
|
92
|
+
} catch {
|
|
93
|
+
// Never an unhandled rejection from a discarded handler: the call reports what it was going
|
|
94
|
+
// to report.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalize(error: unknown, signal: AbortSignal | undefined): void {
|
|
99
|
+
if (!(error instanceof RpcError)) return;
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
signal?.aborted &&
|
|
103
|
+
(error.code === GrpcStatusCode[GrpcStatusCode.INTERNAL] ||
|
|
104
|
+
error.code === GrpcStatusCode[GrpcStatusCode.CANCELLED])
|
|
105
|
+
) {
|
|
106
|
+
error.code = abortStatus(signal.reason);
|
|
107
|
+
|
|
108
|
+
// Text the abort reason supplied is local and holds nothing to decode; anything else came off
|
|
109
|
+
// the wire, even though the abort is what the caller sees. Settled here for the other promises
|
|
110
|
+
// the call rejects, which no longer match the codes above.
|
|
111
|
+
if (isAbortReasonText(error.message, signal.reason)) markStatusMessageRead(error);
|
|
112
|
+
else decodeStatusMessageOnce(error);
|
|
113
|
+
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
decodeStatusMessageOnce(error);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeRejections(promises: Promise<unknown>[], signal: AbortSignal | undefined) {
|
|
121
|
+
for (const promise of promises) {
|
|
122
|
+
// Registered before the call is returned, so it runs before the consumer's own await.
|
|
123
|
+
promise.then(undefined, (error: unknown) => normalizeGrpcError(error, signal));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* `GrpcWebFetchTransport` from `@protobuf-ts/grpcweb-transport`, subclassed to decode status
|
|
129
|
+
* messages and to code an aborted call from its reason rather than as `INTERNAL`.
|
|
130
|
+
*
|
|
131
|
+
* `SuiGrpcClient` builds one by default. A transport imported from `@protobuf-ts/grpcweb-transport`
|
|
132
|
+
* keeps that package's behaviour.
|
|
133
|
+
*/
|
|
134
|
+
export class GrpcWebFetchTransport extends UpstreamGrpcWebFetchTransport {
|
|
135
|
+
override unary<I extends object, O extends object>(
|
|
136
|
+
method: MethodInfo<I, O>,
|
|
137
|
+
input: I,
|
|
138
|
+
options: RpcOptions,
|
|
139
|
+
): UnaryCall<I, O> {
|
|
140
|
+
const call = super.unary(method, input, options);
|
|
141
|
+
|
|
142
|
+
normalizeRejections([call.headers, call.response, call.status, call.trailers], options.abort);
|
|
143
|
+
|
|
144
|
+
return call;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override serverStreaming<I extends object, O extends object>(
|
|
148
|
+
method: MethodInfo<I, O>,
|
|
149
|
+
input: I,
|
|
150
|
+
options: RpcOptions,
|
|
151
|
+
): ServerStreamingCall<I, O> {
|
|
152
|
+
const call = super.serverStreaming(method, input, options);
|
|
153
|
+
|
|
154
|
+
// A finite stream is often read through `responses` without awaiting `status`.
|
|
155
|
+
call.responses.onError((error) => normalizeGrpcError(error, options.abort));
|
|
156
|
+
normalizeRejections([call.headers, call.status, call.trailers], options.abort);
|
|
157
|
+
|
|
158
|
+
return call;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
coinWithBalance,
|
|
38
38
|
createBalance,
|
|
39
39
|
} from './intents/CoinWithBalance.js';
|
|
40
|
-
import type { ClientWithCoreApi } from '../client/core.js';
|
|
41
40
|
|
|
42
41
|
export type TransactionObjectArgument =
|
|
43
42
|
| Exclude<InferInput<typeof ArgumentSchema>, { Input: unknown; type?: 'pure' }>
|
|
@@ -802,9 +801,7 @@ export class Transaction {
|
|
|
802
801
|
|
|
803
802
|
/** Derive transaction digest */
|
|
804
803
|
async getDigest(
|
|
805
|
-
options: {
|
|
806
|
-
client?: ClientWithCoreApi;
|
|
807
|
-
} = {},
|
|
804
|
+
options: Pick<BuildTransactionOptions, 'client' | 'assumeSufficientAddressBalances'> = {},
|
|
808
805
|
): Promise<string> {
|
|
809
806
|
await this.prepareForSerialization(options);
|
|
810
807
|
await this.#prepareBuild(options);
|