@@ -672,6 +672,97 @@ describe('Sui Transfer Builder', () => {
672672 } ) ;
673673 } ) ;
674674
675+ describe ( 'large amounts exceeding Number.MAX_SAFE_INTEGER' , ( ) => {
676+ // Number.MAX_SAFE_INTEGER = 9007199254740991.
677+ // Using Number() for amounts above this silently rounds to the nearest representable float.
678+ // 9007199254740993 (MAX_SAFE_INTEGER + 2) is NOT exactly representable as a JS Number — it
679+ // rounds down to 9007199254740992 — so BCS would encode the wrong value.
680+ // BigInt preserves the exact value and is the correct type for BCS u64 encoding.
681+ //
682+ // The core check in each test is tx.outputs[0].value === LARGE_AMOUNT: this reads from the
683+ // in-memory builder state (a BigInt, not BCS-decoded), so it accurately distinguishes
684+ // BigInt() vs Number() encoding at build time.
685+ const LARGE_AMOUNT = '9007199254740993' ; // MAX_SAFE_INTEGER + 2
686+
687+ it ( 'should build a self-pay transfer (Path 2) with amount > Number.MAX_SAFE_INTEGER and encode it precisely' , async function ( ) {
688+ const txBuilder = factory . getTransferBuilder ( ) ;
689+ txBuilder . type ( SuiTransactionType . Transfer ) ;
690+ txBuilder . sender ( testData . sender . address ) ;
691+ txBuilder . send ( [ { address : testData . recipients [ 0 ] . address , amount : LARGE_AMOUNT } ] ) ;
692+ txBuilder . gasData ( testData . gasData ) ;
693+
694+ const tx = await txBuilder . build ( ) ;
695+ should . equal ( tx . type , TransactionType . Send ) ;
696+
697+ // With Number() the amount would be silently rounded to 9007199254740992 — BigInt preserves the exact value
698+ tx . outputs . length . should . equal ( 1 ) ;
699+ tx . outputs [ 0 ] . value . should . equal ( LARGE_AMOUNT ) ;
700+
701+ const rawTx = tx . toBroadcastFormat ( ) ;
702+ should . equal ( utils . isValidRawTransaction ( rawTx ) , true ) ;
703+ } ) ;
704+
705+ it ( 'should build a sponsored transfer with coin objects (Path 1a) with amount > Number.MAX_SAFE_INTEGER and encode it precisely' , async function ( ) {
706+ const inputObjects = testData . generateObjects ( 2 ) ;
707+ const sponsoredGasData = {
708+ ...testData . gasData ,
709+ owner : testData . feePayer . address ,
710+ } ;
711+
712+ const txBuilder = factory . getTransferBuilder ( ) ;
713+ txBuilder . type ( SuiTransactionType . Transfer ) ;
714+ txBuilder . sender ( testData . sender . address ) ;
715+ txBuilder . send ( [ { address : testData . recipients [ 0 ] . address , amount : LARGE_AMOUNT } ] ) ;
716+ txBuilder . gasData ( sponsoredGasData ) ;
717+ txBuilder . inputObjects ( inputObjects ) ;
718+
719+ const tx = await txBuilder . build ( ) ;
720+ should . equal ( tx . type , TransactionType . Send ) ;
721+
722+ // With Number() the amount would be silently rounded to 9007199254740992 — BigInt preserves the exact value
723+ tx . outputs . length . should . equal ( 1 ) ;
724+ tx . outputs [ 0 ] . value . should . equal ( LARGE_AMOUNT ) ;
725+
726+ const rawTx = tx . toBroadcastFormat ( ) ;
727+ should . equal ( utils . isValidRawTransaction ( rawTx ) , true ) ;
728+ } ) ;
729+
730+ it ( 'should build a sponsored addr-balance-only transfer (Path 1b) with amount > Number.MAX_SAFE_INTEGER and round-trip correctly' , async function ( ) {
731+ const sponsoredGasData = {
732+ ...testData . gasData ,
733+ owner : testData . feePayer . address ,
734+ } ;
735+
736+ const txBuilder = factory . getTransferBuilder ( ) ;
737+ txBuilder . type ( SuiTransactionType . Transfer ) ;
738+ txBuilder . sender ( testData . sender . address ) ;
739+ txBuilder . send ( [ { address : testData . recipients [ 0 ] . address , amount : LARGE_AMOUNT } ] ) ;
740+ txBuilder . gasData ( sponsoredGasData ) ;
741+ txBuilder . fundsInAddressBalance ( LARGE_AMOUNT ) ;
742+
743+ const tx = await txBuilder . build ( ) ;
744+ should . equal ( tx . type , TransactionType . Send ) ;
745+
746+ // With Number() the amount would be silently rounded to 9007199254740992 — BigInt preserves the exact value
747+ tx . outputs . length . should . equal ( 1 ) ;
748+ tx . outputs [ 0 ] . value . should . equal ( LARGE_AMOUNT ) ;
749+
750+ const rawTx = tx . toBroadcastFormat ( ) ;
751+ should . equal ( utils . isValidRawTransaction ( rawTx ) , true ) ;
752+
753+ // Full round-trip: rebuilding from BCS bytes must produce identical bytes
754+ const rebuilder = factory . from ( rawTx ) ;
755+ rebuilder . addSignature ( { pub : testData . sender . publicKey } , Buffer . from ( testData . sender . signatureHex ) ) ;
756+ const rebuiltTx = await rebuilder . build ( ) ;
757+ rebuiltTx . toBroadcastFormat ( ) . should . equal ( rawTx ) ;
758+
759+ const recipients = utils . getRecipients (
760+ ( rebuiltTx as SuiTransaction < TransferProgrammableTransaction > ) . suiTransaction
761+ ) ;
762+ recipients [ 0 ] . amount . should . equal ( LARGE_AMOUNT ) ;
763+ } ) ;
764+ } ) ;
765+
675766 describe ( 'BalanceWithdrawal BCS encoding (FundsWithdrawal format)' , ( ) => {
676767 const AMOUNT = '100000000' ; // 0.1 SUI in MIST
677768 const sponsoredGasData = {
0 commit comments