@@ -31,6 +31,7 @@ import {
3131 getAmountBasedOnEndianness ,
3232 KeyPair ,
3333 Sol ,
34+ SolConsolidationRecoveryOptions ,
3435 SolRecoveryOptions ,
3536 SolVerifyTransactionOptions ,
3637 Tsol ,
@@ -2592,6 +2593,37 @@ describe('SOL:', function () {
25922593 sandBox . assert . callCount ( solCoin . getDataFromNode , 4 ) ;
25932594 } ) ;
25942595
2596+ it ( 'should recover a txn for unsigned sweep recoveries when userKey/backupKey are empty strings' , async function ( ) {
2597+ // Regression test: WRW's cold-wallet sweep form sends userKey/backupKey as empty strings
2598+ // (rather than omitting them) to satisfy TS typing. Empty string and undefined must be
2599+ // handled identically by the unsigned-sweep gate (`!params.walletPassphrase`) and by
2600+ // isMpcv2SigningMaterial's early `!walletPassphrase` short-circuit.
2601+ const unsignedSweepTxn = ( await basecoin . recover ( {
2602+ userKey : '' ,
2603+ backupKey : '' ,
2604+ bitgoKey : testData . keys . bitgoKey ,
2605+ recoveryDestination : testData . keys . destinationPubKey ,
2606+ durableNonce : {
2607+ publicKey : testData . keys . durableNoncePubKey ,
2608+ secretKey : testData . keys . durableNoncePrivKey ,
2609+ } ,
2610+ } ) ) as MPCSweepTxs ;
2611+
2612+ unsignedSweepTxn . should . not . be . empty ( ) ;
2613+ unsignedSweepTxn . txRequests [ 0 ] . transactions [ 0 ] . unsignedTx . should . hasOwnProperty ( 'serializedTx' ) ;
2614+ should . equal ( unsignedSweepTxn . txRequests [ 0 ] . transactions [ 0 ] . unsignedTx . scanIndex , 0 ) ;
2615+
2616+ const unsignedSweepTxnDeserialize = new Transaction ( coin ) ;
2617+ unsignedSweepTxnDeserialize . fromRawTransaction (
2618+ unsignedSweepTxn . txRequests [ 0 ] . transactions [ 0 ] . unsignedTx . serializedTx
2619+ ) ;
2620+ const unsignedSweepTxnJson = unsignedSweepTxnDeserialize . toJson ( ) ;
2621+
2622+ should . equal ( unsignedSweepTxnJson . nonce , testData . SolInputData . durableNonceBlockhash ) ;
2623+ should . equal ( unsignedSweepTxnJson . feePayer , testData . accountInfo . bs58EncodedPublicKey ) ;
2624+ should . equal ( unsignedSweepTxnJson . numSignatures , testData . SolInputData . unsignedSweepSignatures ) ;
2625+ } ) ;
2626+
25952627 it ( 'should handle error in recover function if a required field is missing/incorrect' , async function ( ) {
25962628 // missing userkey
25972629 await basecoin
@@ -3889,6 +3921,41 @@ describe('SOL:', function () {
38893921 should . equal ( latestBlockhashTxnJson1 . numSignatures , testData . SolInputData . unsignedSweepSignatures ) ;
38903922 } ) ;
38913923
3924+ it ( 'should build signed token consolidation recoveries (hot wallet)' , async function ( ) {
3925+ // Regression test: hot-wallet SPL-token consolidation must produce a *signed* transaction
3926+ // when userKey/backupKey/walletPassphrase are supplied — mirrors the WRW hot-wallet flow.
3927+ const res = ( await basecoin . recoverConsolidations ( {
3928+ userKey : testData . wrwUser . userKey ,
3929+ backupKey : testData . wrwUser . backupKey ,
3930+ bitgoKey : testData . wrwUser . bitgoKey ,
3931+ walletPassphrase : testData . wrwUser . walletPassphrase ,
3932+ startingScanIndex : 3 ,
3933+ endingScanIndex : 5 ,
3934+ tokenContractAddress : usdtMintAddress ,
3935+ durableNonces : durableNonces ,
3936+ } ) ) as MPCTxs ;
3937+
3938+ res . should . not . be . empty ( ) ;
3939+ res . transactions . length . should . equal ( 1 ) ;
3940+
3941+ const txn1 = res . transactions [ 0 ] as MPCTx ;
3942+ txn1 . should . hasOwnProperty ( 'serializedTx' ) ;
3943+ txn1 . should . hasOwnProperty ( 'scanIndex' ) ;
3944+ ( txn1 . scanIndex ?? 0 ) . should . equal ( 4 ) ;
3945+
3946+ const tokenConsolidationTxnDeserialize = new Transaction ( coin ) ;
3947+ tokenConsolidationTxnDeserialize . fromRawTransaction ( txn1 . serializedTx ) ;
3948+ const tokenConsolidationTxnJson = tokenConsolidationTxnDeserialize . toJson ( ) ;
3949+
3950+ const nonce1 = testData . SolResponses . getAccountInfoResponse . body . result . value . data . parsed . info . blockhash ;
3951+ should . equal ( tokenConsolidationTxnJson . nonce , nonce1 ) ;
3952+ should . equal ( tokenConsolidationTxnJson . feePayer , testData . wrwUser . walletAddress5 ) ;
3953+ // Signed (durable nonce + recovery signature) must have MORE signatures than an unsigned sweep
3954+ // of the same transaction shape — proves walletPassphrase actually drove a signed path.
3955+ should . equal ( tokenConsolidationTxnJson . numSignatures , testData . SolInputData . durableNonceSignatures ) ;
3956+ tokenConsolidationTxnJson . numSignatures . should . be . above ( testData . SolInputData . unsignedSweepSignatures ) ;
3957+ } ) ;
3958+
38923959 it ( 'should skip building consolidate transaction if balance is equal to zero' , async function ( ) {
38933960 await basecoin
38943961 . recoverConsolidations ( {
@@ -3903,6 +3970,18 @@ describe('SOL:', function () {
39033970 . should . rejectedWith ( 'Did not find an address with funds to recover' ) ;
39043971 } ) ;
39053972
3973+ it ( 'should throw if bitgoKey is missing' , async ( ) => {
3974+ await basecoin
3975+ . recoverConsolidations ( {
3976+ userKey : testData . wrwUser . userKey ,
3977+ backupKey : testData . wrwUser . backupKey ,
3978+ startingScanIndex : 1 ,
3979+ endingScanIndex : 2 ,
3980+ durableNonces : durableNonces ,
3981+ } as SolConsolidationRecoveryOptions )
3982+ . should . be . rejectedWith ( 'missing bitgoKey' ) ;
3983+ } ) ;
3984+
39063985 it ( 'should throw if startingScanIndex is not ge to 1' , async ( ) => {
39073986 await basecoin
39083987 . recoverConsolidations ( {
0 commit comments