Skip to content

Commit 4247d7e

Browse files
fix(sdk-coin-sui): handle epoch round trip
TICKET: CSHLD-601
1 parent 97ada38 commit 4247d7e

File tree

3 files changed

+53
-13
lines changed

3 files changed

+53
-13
lines changed

modules/sdk-coin-sui/src/lib/mystenlab/builder/TransactionDataBlock.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,6 @@ import { TransactionType, TransactionBlockInput } from './Transactions';
2020
import { BuilderCallArg, PureCallArg } from './Inputs';
2121
import { create } from './utils';
2222

23-
export const TransactionExpiration = optional(
24-
nullable(
25-
union([
26-
object({ Epoch: integer() }),
27-
object({ None: union([literal(true), literal(null)]) }),
28-
object({ ValidDuring: object({ minEpoch: integer(), maxEpoch: integer(), chain: string(), nonce: integer() }) }),
29-
])
30-
)
31-
);
32-
export type TransactionExpiration = Infer<typeof TransactionExpiration>;
33-
3423
const SuiAddress = string();
3524

3625
const StringEncodedBigint = define<string>('StringEncodedBigint', (val) => {
@@ -44,6 +33,17 @@ const StringEncodedBigint = define<string>('StringEncodedBigint', (val) => {
4433
}
4534
});
4635

36+
export const TransactionExpiration = optional(
37+
nullable(
38+
union([
39+
object({ Epoch: StringEncodedBigint }),
40+
object({ None: union([literal(true), literal(null)]) }),
41+
object({ ValidDuring: object({ minEpoch: integer(), maxEpoch: integer(), chain: string(), nonce: integer() }) }),
42+
])
43+
)
44+
);
45+
export type TransactionExpiration = Infer<typeof TransactionExpiration>;
46+
4747
const GasConfig = object({
4848
budget: optional(StringEncodedBigint),
4949
price: optional(StringEncodedBigint),

modules/sdk-coin-sui/src/lib/mystenlab/types/sui-bcs.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export type ValidDuringExpiration = {
123123
*
124124
* Indications the expiration time for a transaction.
125125
*/
126-
export type TransactionExpiration = { None: null } | { Epoch: number } | { ValidDuring: ValidDuringExpiration };
126+
export type TransactionExpiration = { None: null } | { Epoch: number | bigint | string } | { ValidDuring: ValidDuringExpiration };
127127

128128
// Move name of the Vector type.
129129
const VECTOR = 'vector';
@@ -155,7 +155,6 @@ const BCS_SPEC: TypeSchema = {
155155
CallArg: {
156156
Pure: [VECTOR, BCS.U8],
157157
Object: 'ObjectArg',
158-
ObjVec: [VECTOR, 'ObjectArg'],
159158
BalanceWithdrawal: 'BalanceWithdrawal',
160159
},
161160
TypeTag: {

modules/sdk-coin-sui/test/unit/transactionBuilder/transferBuilder.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,5 +470,46 @@ describe('Sui Transfer Builder', () => {
470470
const rawTx = tx.toBroadcastFormat();
471471
should.equal(utils.isValidRawTransaction(rawTx), true);
472472
});
473+
474+
it('should round-trip a self-pay transfer with Epoch expiration via fromBytes', async function () {
475+
// Regression test for the BigInt round-trip bug:
476+
// BCS.U64 deserializes u64 as BigInt, but the previous superstruct schema used integer()
477+
// which rejected BigInt, causing fromBytes() to throw a StructError at "expiration".
478+
// StringEncodedBigint now accepts string | number | bigint, fixing the round-trip.
479+
const gasDataNoPayment = {
480+
...testData.gasDataWithoutGasPayment,
481+
payment: [],
482+
};
483+
484+
const txBuilder = factory.getTransferBuilder();
485+
txBuilder.type(SuiTransactionType.Transfer);
486+
txBuilder.sender(testData.sender.address);
487+
txBuilder.send(testData.recipients);
488+
txBuilder.gasData(gasDataNoPayment);
489+
txBuilder.fundsInAddressBalance(FUNDS_IN_ADDRESS_BALANCE);
490+
txBuilder.expiration({ Epoch: 324 }); // number input
491+
492+
const tx = await txBuilder.build();
493+
const rawTx = tx.toBroadcastFormat();
494+
should.equal(utils.isValidRawTransaction(rawTx), true);
495+
496+
// fromBytes must not throw StructError — this was the failing case before the fix
497+
should.doesNotThrow(() => {
498+
const rebuilder = factory.from(rawTx);
499+
should.exist(rebuilder);
500+
});
501+
502+
// Full round-trip: rebuilt tx must serialize identically
503+
const rebuilder = factory.from(rawTx);
504+
rebuilder.addSignature({ pub: testData.sender.publicKey }, Buffer.from(testData.sender.signatureHex));
505+
const rebuiltTx = await rebuilder.build();
506+
rebuiltTx.toBroadcastFormat().should.equal(rawTx);
507+
508+
// Epoch value must survive the round-trip regardless of BigInt/number representation
509+
const rebuiltJson = rebuiltTx.toJson();
510+
const epochVal = (rebuiltJson.expiration as any)?.Epoch;
511+
should.exist(epochVal);
512+
Number(epochVal).should.equal(324);
513+
});
473514
});
474515
});

0 commit comments

Comments
 (0)