Skip to content

Commit 566b3fd

Browse files
authored
Add address balance support to CoinWithBalance and core API (#773)
* Add address balance support to CoinWithBalance and core API * Add json rpc address balances * change expiration default * Add transction resolution for graphql * add graphql address balances
1 parent 742b7c2 commit 566b3fd

File tree

37 files changed

+3909
-2828
lines changed

37 files changed

+3909
-2828
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mysten/sui': minor
3+
---
4+
5+
Transactions now default the expiration to the current epoch + 1 using `ValidDuring`

packages/dapp-kit-next/packages/dapp-kit-core/src/utils/transaction-result.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
export type TransactionResultWithEffects = SuiClientTypes.TransactionResult<{
1212
effects: true;
1313
transaction: true;
14+
bcs: true;
1415
}>;
1516

1617
export function buildTransactionResult(
@@ -30,7 +31,7 @@ export function buildTransactionResult(
3031
);
3132
}
3233

33-
const txResult: SuiClientTypes.Transaction<{ effects: true; transaction: true }> = {
34+
const txResult: SuiClientTypes.Transaction<{ effects: true; transaction: true; bcs: true }> = {
3435
digest,
3536
signatures: [signature],
3637
epoch: null,
@@ -40,6 +41,7 @@ export function buildTransactionResult(
4041
balanceChanges: undefined,
4142
events: undefined,
4243
objectTypes: undefined,
44+
bcs: transactionBytes,
4345
};
4446

4547
return status.success

packages/docs/content/sui/clients/core.mdx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,17 +260,30 @@ console.log('Digest:', result.Transaction.digest);
260260
Fetch a transaction by digest.
261261

262262
```typescript
263-
const tx = await client.core.getTransaction({
263+
const result = await client.core.getTransaction({
264264
digest: 'ABC123...',
265265
include: {
266266
effects: true,
267267
events: true,
268-
input: true,
268+
transaction: true,
269269
},
270270
});
271271

272-
console.log(tx.Transaction?.digest);
273-
console.log(tx.Transaction?.effects);
272+
console.log(result.Transaction?.digest);
273+
console.log(result.Transaction?.effects);
274+
console.log(result.Transaction?.transaction?.sender);
275+
```
276+
277+
You can also fetch raw BCS-encoded transaction bytes:
278+
279+
```typescript
280+
const result = await client.core.getTransaction({
281+
digest: 'ABC123...',
282+
include: { bcs: true },
283+
});
284+
285+
// Raw BCS bytes for the transaction
286+
console.log(result.Transaction?.bcs); // Uint8Array
274287
```
275288

276289
### waitForTransaction
@@ -391,11 +404,12 @@ Most methods accept an `include` parameter to control what data is returned:
391404

392405
```typescript
393406
{
394-
effects: boolean, // Transaction effects
407+
effects: boolean, // Transaction effects (BCS-encoded)
395408
events: boolean, // Emitted events
396-
input: boolean, // Transaction input data
409+
transaction: boolean, // Parsed transaction data (sender, gas, inputs, commands)
397410
balanceChanges: boolean, // Balance changes
398-
objectChanges: boolean, // Object changes
411+
objectTypes: boolean, // Map of object IDs to their types for changed objects
412+
bcs: boolean, // Raw BCS-encoded transaction bytes
399413
}
400414
```
401415

packages/docs/content/sui/migrations/sui-2.0/sui.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,3 +326,16 @@ The `legacyAddress` parameter is now **required** for all zkLogin address comput
326326
- toZkLoginPublicIdentifier(addressSeed, iss)
327327
+ toZkLoginPublicIdentifier(addressSeed, iss, { legacyAddress: false })
328328
```
329+
330+
## Default Transaction Expiration
331+
332+
Transactions now default the expiration to the current epoch + 1 using `ValidDuring` when built with
333+
a client. This provides replay protection for all transactions without requiring explicit
334+
configuration.
335+
336+
**To preserve the old behavior** (no expiration), explicitly set the expiration to `None`:
337+
338+
```typescript
339+
const tx = new Transaction();
340+
tx.setExpiration({ None: true });
341+
```

packages/docs/content/sui/transaction-building/offline.mdx

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,73 @@ title: Building Offline
33
---
44

55
To build a transaction offline (with no `client` required), you need to fully define all of your
6-
input values and gas configuration (see the following example). For pure values, you can provide a
7-
`Uint8Array` which is used directly in the transaction. For objects, you can use the `Inputs` helper
8-
to construct an object reference.
6+
inputs, gas configuration, and expiration.
7+
8+
## Required Configuration
9+
10+
When building offline, you must set the following:
11+
12+
- **Sender address** - The address that will execute the transaction
13+
- **Gas price** - The price per gas unit (can be obtained from the network beforehand)
14+
- **Gas budget** - The maximum gas to spend on this transaction
15+
- **Gas payment** - One or more coin object references to use for gas, or an empty array for Address
16+
Balances
17+
- **Expiration** - Only needed when using address balances for gas
918

1019
```tsx
11-
import { Inputs } from '@mysten/sui/transactions';
20+
import { Transaction } from '@mysten/sui/transactions';
1221

13-
// for owned or immutable objects
14-
tx.object(Inputs.ObjectRef({ digest, objectId, version }));
22+
const { referenceGasPrice } = await client.getReferenceGasPrice();
1523

16-
// for shared objects
17-
tx.object(Inputs.SharedObjectRef({ objectId, initialSharedVersion, mutable }));
24+
const tx = new Transaction();
25+
26+
tx.setSender('0x<your-address>');
27+
tx.setGasPrice(referenceGasPrice);
28+
tx.setGasBudget(50_000_000);
29+
tx.setGasPayment([
30+
{
31+
objectId: '0x<gas-coin-object-id>',
32+
version: '<object-version>',
33+
digest: '<object-digest>',
34+
},
35+
]);
36+
37+
// Build the transaction without a client
38+
const bytes = await tx.build();
1839
```
1940

20-
You can then omit the `client` object when calling `build` on the transaction. Any required data
21-
that is missing throws an error.
41+
## Object References
42+
43+
For objects used in your transaction, you must provide full object references using the `Inputs`
44+
helper:
45+
46+
```tsx
47+
import { Inputs } from '@mysten/sui/transactions';
48+
49+
// For owned or immutable objects
50+
tx.object(
51+
Inputs.ObjectRef({
52+
objectId: '0x<object-id>',
53+
version: '<object-version>',
54+
digest: '<object-digest>',
55+
}),
56+
);
57+
58+
// For shared objects
59+
tx.object(
60+
Inputs.SharedObjectRef({
61+
objectId: '0x<object-id>',
62+
initialSharedVersion: '<initial-shared-version>',
63+
mutable: true,
64+
}),
65+
);
66+
67+
// For receiving objects (objects being received by another object)
68+
tx.object(
69+
Inputs.ReceivingRef({
70+
objectId: '0x<object-id>',
71+
version: '<object-version>',
72+
digest: '<object-digest>',
73+
}),
74+
);
75+
```

packages/kiosk/test/e2e/globalSetup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ declare module 'vitest' {
1717
const SUI_TOOLS_TAG =
1818
process.env.SUI_TOOLS_TAG ||
1919
(process.arch === 'arm64'
20-
? '62eeb2716b43d1248efcf19c208086840f5a0695-arm64'
21-
: '62eeb2716b43d1248efcf19c208086840f5a0695');
20+
? 'cc8e624a969eed2066ff5f25c64c42a34143b0a6-arm64'
21+
: 'cc8e624a969eed2066ff5f25c64c42a34143b0a6');
2222

2323
export default async function setup(project: TestProject) {
2424
console.log('Starting test containers');

packages/sui/src/client/core-resolver.ts

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,53 @@ async function setGasBudget(transactionData: TransactionDataBuilder, client: Cli
112112
// The current default is just picking _all_ coins we can which may not be ideal.
113113
async function setGasPayment(transactionData: TransactionDataBuilder, client: ClientWithCoreApi) {
114114
if (!transactionData.gasData.payment) {
115-
const coins = await client.core.listCoins({
116-
owner: transactionData.gasData.owner || transactionData.sender!,
117-
coinType: SUI_TYPE_ARG,
115+
const gasPayer = transactionData.gasData.owner ?? transactionData.sender!;
116+
let usesGasCoin = false;
117+
let withdrawals = 0n;
118+
119+
transactionData.mapArguments((arg) => {
120+
if (arg.$kind === 'GasCoin') {
121+
usesGasCoin = true;
122+
} else if (arg.$kind === 'Input') {
123+
const input = transactionData.inputs[arg.Input];
124+
125+
if (input.$kind === 'FundsWithdrawal') {
126+
const withdrawalOwner = input.FundsWithdrawal.withdrawFrom.Sender
127+
? transactionData.sender
128+
: gasPayer;
129+
130+
if (withdrawalOwner === gasPayer) {
131+
if (input.FundsWithdrawal.reservation.$kind === 'MaxAmountU64') {
132+
withdrawals += BigInt(input.FundsWithdrawal.reservation.MaxAmountU64);
133+
}
134+
}
135+
}
136+
}
137+
138+
return arg;
118139
});
119140

141+
const [suiBalance, coins] = await Promise.all([
142+
usesGasCoin || !transactionData.gasData.owner
143+
? null
144+
: client.core.getBalance({
145+
owner: transactionData.gasData.owner,
146+
}),
147+
client.core.listCoins({
148+
owner: transactionData.gasData.owner || transactionData.sender!,
149+
coinType: SUI_TYPE_ARG,
150+
}),
151+
]);
152+
153+
if (
154+
suiBalance?.balance.addressBalance &&
155+
BigInt(suiBalance.balance.addressBalance) >=
156+
BigInt(transactionData.gasData.budget || '0') + withdrawals
157+
) {
158+
transactionData.gasData.payment = [];
159+
return;
160+
}
161+
120162
const paymentCoins = coins.objects
121163
// Filter out coins that are also used as input:
122164
.filter((coin) => {
@@ -130,19 +172,19 @@ async function setGasPayment(transactionData: TransactionDataBuilder, client: Cl
130172

131173
return !matchingInput;
132174
})
133-
.map((coin) => ({
134-
objectId: coin.objectId,
135-
digest: coin.digest,
136-
version: coin.version,
137-
}));
175+
.map((coin) =>
176+
parse(ObjectRefSchema, {
177+
objectId: coin.objectId,
178+
digest: coin.digest,
179+
version: coin.version,
180+
}),
181+
);
138182

139183
if (!paymentCoins.length) {
140184
throw new Error('No valid gas coins found for the transaction.');
141185
}
142186

143-
transactionData.gasData.payment = paymentCoins.map((payment) =>
144-
parse(ObjectRefSchema, payment),
145-
);
187+
transactionData.gasData.payment = paymentCoins;
146188
}
147189
}
148190

@@ -151,17 +193,6 @@ async function setExpiration(
151193
client: ClientWithCoreApi,
152194
existingSystemState: SystemStateData | null,
153195
) {
154-
const hasOwnedInputs = transactionData.inputs.some((input) => {
155-
return input.Object?.ImmOrOwnedObject || input.Object?.Receiving;
156-
});
157-
158-
const hasGasPayment =
159-
transactionData.gasData.payment && transactionData.gasData.payment.length > 0;
160-
161-
if (hasOwnedInputs || hasGasPayment) {
162-
return;
163-
}
164-
165196
const [systemState, { chainIdentifier }] = await Promise.all([
166197
existingSystemState ?? client.core.getCurrentSystemState().then((r) => r.systemState),
167198
client.core.getChainIdentifier(),

0 commit comments

Comments
 (0)