@@ -22,6 +22,7 @@ import {
22
22
makeRandomKey ,
23
23
getSharedSecret ,
24
24
BulkWalletShareOptions ,
25
+ AcceptShareOptionsRequest ,
25
26
KeychainWithEncryptedPrv ,
26
27
WalletWithKeychains ,
27
28
multisigTypes ,
@@ -1872,6 +1873,283 @@ describe('V2 Wallets:', function () {
1872
1873
] ,
1873
1874
} ) ;
1874
1875
} ) ;
1876
+
1877
+ it ( 'should handle 413 payload too large error with smart retry' , async ( ) => {
1878
+ const walletPassphrase = 'bitgo1234' ;
1879
+ const fromUserPrv = Math . random ( ) ;
1880
+ const keychainTest : OptionalKeychainEncryptedKey = {
1881
+ encryptedPrv : bitgo . encrypt ( { input : fromUserPrv . toString ( ) , password : walletPassphrase } ) ,
1882
+ } ;
1883
+ const userPrv = decryptKeychainPrivateKey ( bitgo , keychainTest , walletPassphrase ) ;
1884
+ if ( ! userPrv ) {
1885
+ throw new Error ( 'Unable to decrypt user keychain' ) ;
1886
+ }
1887
+
1888
+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
1889
+ const path = 'm/999999/1/1' ;
1890
+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
1891
+
1892
+ const eckey = makeRandomKey ( ) ;
1893
+ const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
1894
+ // Pad the private key with additional data to make it larger before encrypting
1895
+ const newEncryptedPrv = bitgo . encrypt ( { password : secret , input : userPrv } ) ;
1896
+ const keychain = {
1897
+ path : path ,
1898
+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
1899
+ encryptedPrv : newEncryptedPrv ,
1900
+ toPubKey : pubkey ,
1901
+ pub : pubkey ,
1902
+ } ;
1903
+ const shareIds = Array . from ( { length : 20 } , ( _ , i ) => `share${ i + 1 } ` ) ;
1904
+
1905
+ // Mock listSharesV2 to return 25 shares
1906
+ const shares = shareIds . map ( ( id , index ) => ( {
1907
+ id,
1908
+ coin : 'tsol' ,
1909
+ walletLabel : `testing${ index } ` ,
1910
+ fromUser : 'dummyFromUser' ,
1911
+ toUser : 'dummyToUser' ,
1912
+ wallet : `wallet${ index } ` ,
1913
+ permissions : [ 'spend' ] ,
1914
+ state : 'active' as const ,
1915
+ keychain : keychain ,
1916
+ } ) ) ;
1917
+
1918
+ sinon . stub ( Wallets . prototype , 'listSharesV2' ) . resolves ( {
1919
+ incoming : shares ,
1920
+ outgoing : [ ] ,
1921
+ } ) ;
1922
+
1923
+ const myEcdhKeychain = await bitgo . keychains ( ) . create ( ) ;
1924
+ sinon . stub ( bitgo , 'getECDHKeychain' ) . resolves ( {
1925
+ encryptedXprv : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
1926
+ } ) ;
1927
+
1928
+ const prvKey = bitgo . decrypt ( {
1929
+ password : walletPassphrase ,
1930
+ input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
1931
+ } ) ;
1932
+ sinon . stub ( bitgo , 'decrypt' ) . returns ( prvKey ) ;
1933
+ sinon . stub ( bitgo , 'encrypt' ) . returns ( userPrv + 'X' . repeat ( 100000 ) ) ;
1934
+ sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
1935
+
1936
+ // Mock bulkAcceptShareRequestWithRetry to track batch sizes
1937
+ const batchSizes : number [ ] = [ ] ;
1938
+
1939
+ nock ( bgUrl )
1940
+ . persist ( ) // This ensures the interceptor remains active for multiple requests
1941
+ . put ( '/api/v2/walletshares/accept' )
1942
+ . reply ( function ( _ , requestBody , cb ) {
1943
+ const params = requestBody [ 'keysForWalletShares' ] as AcceptShareOptionsRequest [ ] ;
1944
+ batchSizes . push ( params . length ) ;
1945
+ if ( Buffer . byteLength ( JSON . stringify ( requestBody ) , 'utf8' ) > 950000 ) {
1946
+ // Simulate 413 error
1947
+ return cb ( null , [ 413 , { error : 'Request Entity Too Large' } ] ) ;
1948
+ }
1949
+ // Return success for smaller batches
1950
+ return cb ( null , [
1951
+ 200 ,
1952
+ {
1953
+ acceptedWalletShares : params . map ( ( param ) => ( {
1954
+ walletShareId : param . walletShareId ,
1955
+ } ) ) ,
1956
+ } ,
1957
+ ] ) ;
1958
+ } ) ;
1959
+
1960
+ const result = await wallets . bulkAcceptShare ( {
1961
+ walletShareIds : shareIds ,
1962
+ userLoginPassword : walletPassphrase ,
1963
+ } ) ;
1964
+
1965
+ // Should have tried with 20 (initial batch size for 25 items), then retried with smaller batches
1966
+ batchSizes . length . should . be . greaterThan ( 1 ) ;
1967
+ batchSizes . should . deepEqual ( [ 9 , 9 , 2 ] ) ; // Initial batch size// Retry batches should be smaller
1968
+
1969
+ result . should . have . property ( 'acceptedWalletShares' ) ;
1970
+ result . acceptedWalletShares . should . be . an . Array ( ) ;
1971
+ result . acceptedWalletShares . length . should . equal ( 20 ) ;
1972
+ result . acceptedWalletShares . forEach ( ( share ) => {
1973
+ share . should . have . property ( 'walletShareId' ) ;
1974
+ share . walletShareId . should . match ( / ^ s h a r e \d + $ / ) ;
1975
+ } ) ;
1976
+ } ) ;
1977
+
1978
+ it ( 'should retry with progressively smaller batch sizes on 413 errors' , async ( ) => {
1979
+ const walletPassphrase = 'bitgo1234' ;
1980
+ const fromUserPrv = Math . random ( ) ;
1981
+ const keychainTest : OptionalKeychainEncryptedKey = {
1982
+ encryptedPrv : bitgo . encrypt ( { input : fromUserPrv . toString ( ) , password : walletPassphrase } ) ,
1983
+ } ;
1984
+ const userPrv = decryptKeychainPrivateKey ( bitgo , keychainTest , walletPassphrase ) ;
1985
+ if ( ! userPrv ) {
1986
+ throw new Error ( 'Unable to decrypt user keychain' ) ;
1987
+ }
1988
+
1989
+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
1990
+ const path = 'm/999999/1/1' ;
1991
+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
1992
+
1993
+ const eckey = makeRandomKey ( ) ;
1994
+ const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
1995
+ const newEncryptedPrv = bitgo . encrypt ( { password : secret , input : userPrv } ) ;
1996
+ const keychain = {
1997
+ path : path ,
1998
+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
1999
+ encryptedPrv : newEncryptedPrv ,
2000
+ toPubKey : pubkey ,
2001
+ pub : pubkey ,
2002
+ } ;
2003
+ const shareIds = Array . from ( { length : 20 } , ( _ , i ) => `share${ i + 1 } ` ) ;
2004
+
2005
+ // Mock listSharesV2
2006
+ const shares = shareIds . map ( ( id , index ) => ( {
2007
+ id,
2008
+ coin : 'tsol' ,
2009
+ walletLabel : `testing${ index } ` ,
2010
+ fromUser : 'dummyFromUser' ,
2011
+ toUser : 'dummyToUser' ,
2012
+ wallet : `wallet${ index } ` ,
2013
+ permissions : [ 'spend' ] ,
2014
+ state : 'active' as const ,
2015
+ keychain : keychain ,
2016
+ } ) ) ;
2017
+
2018
+ sinon . stub ( Wallets . prototype , 'listSharesV2' ) . resolves ( {
2019
+ incoming : shares ,
2020
+ outgoing : [ ] ,
2021
+ } ) ;
2022
+
2023
+ const myEcdhKeychain = await bitgo . keychains ( ) . create ( ) ;
2024
+ sinon . stub ( bitgo , 'getECDHKeychain' ) . resolves ( {
2025
+ encryptedXprv : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
2026
+ } ) ;
2027
+
2028
+ const prvKey = bitgo . decrypt ( {
2029
+ password : walletPassphrase ,
2030
+ input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
2031
+ } ) ;
2032
+ sinon . stub ( bitgo , 'decrypt' ) . returns ( prvKey ) ;
2033
+ sinon . stub ( bitgo , 'encrypt' ) . returns ( userPrv + 'X' . repeat ( 100000 ) ) ;
2034
+ sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
2035
+
2036
+ // Track the sequence of batch sizes attempted
2037
+ const batchSizeAttempts : number [ ] = [ ] ;
2038
+
2039
+ nock ( bgUrl )
2040
+ . persist ( ) // This ensures the interceptor remains active for multiple requests
2041
+ . put ( '/api/v2/walletshares/accept' )
2042
+ . reply ( function ( _ , requestBody : any , cb ) {
2043
+ const params = requestBody [ 'keysForWalletShares' ] as AcceptShareOptionsRequest [ ] ;
2044
+ batchSizeAttempts . push ( params . length ) ;
2045
+
2046
+ // Simulate 413 for batches > 5, success for batches <= 5
2047
+ if ( Buffer . byteLength ( JSON . stringify ( requestBody ) , 'utf8' ) > 600000 ) {
2048
+ // Simulate 413 error
2049
+ return cb ( null , [ 413 , { error : 'Request Entity Too Large' } ] ) ;
2050
+ }
2051
+
2052
+ // Return success for smaller batches
2053
+ return cb ( null , [
2054
+ 200 ,
2055
+ {
2056
+ acceptedWalletShares : params . map ( ( param ) => ( {
2057
+ walletShareId : param . walletShareId || 'test' ,
2058
+ } ) ) ,
2059
+ } ,
2060
+ ] ) ;
2061
+ } ) ;
2062
+
2063
+ const result = await wallets . bulkAcceptShare ( {
2064
+ walletShareIds : shareIds ,
2065
+ userLoginPassword : walletPassphrase ,
2066
+ } ) ;
2067
+
2068
+ // Should see progressive batch size reduction: 20 -> 10 -> 5 (success)
2069
+ batchSizeAttempts . should . containDeep ( [ 9 , 4 , 4 , 4 , 4 , 4 ] ) ;
2070
+
2071
+ result . should . have . property ( 'acceptedWalletShares' ) ;
2072
+ result . acceptedWalletShares . should . be . an . Array ( ) ;
2073
+ result . acceptedWalletShares . length . should . equal ( 20 ) ;
2074
+ result . acceptedWalletShares . forEach ( ( share ) => {
2075
+ share . should . have . property ( 'walletShareId' ) ;
2076
+ share . walletShareId . should . match ( / ^ s h a r e \d + $ / ) ;
2077
+ } ) ;
2078
+ } ) ;
2079
+
2080
+ it ( 'should throw error when batch size cannot be reduced further' , async ( ) => {
2081
+ const walletPassphrase = 'bitgo1234' ;
2082
+ const fromUserPrv = Math . random ( ) ;
2083
+ const keychainTest : OptionalKeychainEncryptedKey = {
2084
+ encryptedPrv : bitgo . encrypt ( { input : fromUserPrv . toString ( ) , password : walletPassphrase } ) ,
2085
+ } ;
2086
+ const userPrv = decryptKeychainPrivateKey ( bitgo , keychainTest , walletPassphrase ) ;
2087
+ if ( ! userPrv ) {
2088
+ throw new Error ( 'Unable to decrypt user keychain' ) ;
2089
+ }
2090
+
2091
+ const toKeychain = utxoLib . bip32 . fromSeed ( Buffer . from ( 'deadbeef02deadbeef02deadbeef02deadbeef02' , 'hex' ) ) ;
2092
+ const path = 'm/999999/1/1' ;
2093
+ const pubkey = toKeychain . derivePath ( path ) . publicKey . toString ( 'hex' ) ;
2094
+
2095
+ const eckey = makeRandomKey ( ) ;
2096
+ const secret = getSharedSecret ( eckey , Buffer . from ( pubkey , 'hex' ) ) . toString ( 'hex' ) ;
2097
+ const newEncryptedPrv = bitgo . encrypt ( { password : secret , input : userPrv } ) ;
2098
+ const keychain = {
2099
+ path : path ,
2100
+ fromPubKey : eckey . publicKey . toString ( 'hex' ) ,
2101
+ encryptedPrv : newEncryptedPrv ,
2102
+ toPubKey : pubkey ,
2103
+ pub : pubkey ,
2104
+ } ;
2105
+ const shareIds = [ 'share1' ] ;
2106
+
2107
+ // Mock listSharesV2
2108
+ sinon . stub ( Wallets . prototype , 'listSharesV2' ) . resolves ( {
2109
+ incoming : [
2110
+ {
2111
+ id : 'share1' ,
2112
+ coin : 'tsol' ,
2113
+ walletLabel : 'testing' ,
2114
+ fromUser : 'dummyFromUser' ,
2115
+ toUser : 'dummyToUser' ,
2116
+ wallet : 'wallet1' ,
2117
+ permissions : [ 'spend' ] ,
2118
+ state : 'active' ,
2119
+ keychain : keychain ,
2120
+ } ,
2121
+ ] ,
2122
+ outgoing : [ ] ,
2123
+ } ) ;
2124
+
2125
+ const myEcdhKeychain = await bitgo . keychains ( ) . create ( ) ;
2126
+ sinon . stub ( bitgo , 'getECDHKeychain' ) . resolves ( {
2127
+ encryptedXprv : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
2128
+ } ) ;
2129
+
2130
+ const prvKey = bitgo . decrypt ( {
2131
+ password : walletPassphrase ,
2132
+ input : bitgo . encrypt ( { input : myEcdhKeychain . xprv , password : walletPassphrase } ) ,
2133
+ } ) ;
2134
+ sinon . stub ( bitgo , 'decrypt' ) . returns ( prvKey ) ;
2135
+ sinon . stub ( moduleBitgo , 'getSharedSecret' ) . resolves ( 'fakeSharedSecret' ) ;
2136
+
2137
+ // Always throw 413 error, even for batch size 1
2138
+ nock ( bgUrl )
2139
+ . persist ( )
2140
+ . put ( '/api/v2/walletshares/accept' )
2141
+ . reply ( function ( _ , _requestBody , cb ) {
2142
+ // Always respond with 413 error
2143
+ return cb ( null , [ 413 , { error : 'Request Entity Too Large' } ] ) ;
2144
+ } ) ;
2145
+
2146
+ await wallets
2147
+ . bulkAcceptShare ( {
2148
+ walletShareIds : shareIds ,
2149
+ userLoginPassword : walletPassphrase ,
2150
+ } )
2151
+ . should . be . rejectedWith ( 'Request Entity Too Large' ) ;
2152
+ } ) ;
1875
2153
} ) ;
1876
2154
1877
2155
describe ( 'bulkUpdateWalletShare' , function ( ) {
0 commit comments