@@ -1728,6 +1728,198 @@ describe('V2 Wallets:', function () {
17281728 } ) ;
17291729 } ) ;
17301730
1731+ describe ( 'Wallet share where userMultiKeyRotationRequired is set true' , ( ) => {
1732+ const sandbox = sinon . createSandbox ( ) ;
1733+
1734+ afterEach ( function ( ) {
1735+ sandbox . verifyAndRestore ( ) ;
1736+ } ) ;
1737+
1738+ it ( 'should throw error when password not provided' , async function ( ) {
1739+ const shareId = 'test_multi_key_1' ;
1740+
1741+ const walletShareNock = nock ( bgUrl )
1742+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
1743+ . reply ( 200 , {
1744+ userMultiKeyRotationRequired : true ,
1745+ permissions : [ 'admin' , 'spend' , 'view' ] ,
1746+ } ) ;
1747+
1748+ await wallets
1749+ . acceptShare ( { walletShareId : shareId } )
1750+ . should . be . rejectedWith ( 'userPassword param must be provided to generate user keychain' ) ;
1751+ walletShareNock . done ( ) ;
1752+ } ) ;
1753+
1754+ it ( 'should successfully accept share with userMultiKeyRotationRequired and send pub and encryptedPrv' , async function ( ) {
1755+ const shareId = 'test_multi_key_2' ;
1756+ const userPassword = 'test_password_123' ;
1757+ const walletId = 'test_wallet_123' ;
1758+
1759+ // Mock wallet share response
1760+ const walletShareNock = nock ( bgUrl )
1761+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
1762+ . reply ( 200 , {
1763+ userMultiKeyRotationRequired : true ,
1764+ permissions : [ 'admin' , 'spend' , 'view' ] ,
1765+ wallet : walletId ,
1766+ } ) ;
1767+
1768+ // Mock keychain creation - baseCoin.keychains().create() returns { prv, pub }
1769+ // We need to create a proper keychain object with prv property
1770+ const testKeychain = bitgo . keychains ( ) . create ( ) ;
1771+ const keychain = {
1772+ prv : testKeychain . xprv , // baseCoin.keychains().create() returns prv (not xprv), but for BTC it's the same
1773+ pub : testKeychain . xpub ,
1774+ } ;
1775+ const encryptedPrv = bitgo . encrypt ( { input : keychain . prv , password : userPassword } ) ;
1776+
1777+ // Mock the updateShare API call
1778+ const acceptShareNock = nock ( bgUrl )
1779+ . post ( `/api/v2/tbtc/walletshare/${ shareId } ` , ( body : any ) => {
1780+ // Verify that pub and encryptedPrv are included
1781+ if ( body . walletShareId !== shareId || body . state !== 'accepted' || ! body . pub || ! body . encryptedPrv ) {
1782+ return false ;
1783+ }
1784+ // Verify pub is a valid format (xpub for BTC)
1785+ if ( ! body . pub . startsWith ( 'xpub' ) ) {
1786+ return false ;
1787+ }
1788+ return true ;
1789+ } )
1790+ . reply ( 200 , { changed : true , state : 'accepted' } ) ;
1791+
1792+ // Stub keychains().create() to return our test keychain
1793+ const keychainsStub = sandbox . stub ( wallets . baseCoin . keychains ( ) , 'create' ) . resolves ( keychain ) ;
1794+
1795+ // Stub bitgo.encrypt to return our encrypted value
1796+ const encryptStub = sandbox . stub ( bitgo , 'encrypt' ) . returns ( encryptedPrv ) ;
1797+
1798+ const res = await wallets . acceptShare ( { walletShareId : shareId , userPassword } ) ;
1799+ should . equal ( res . changed , true ) ;
1800+ should . equal ( res . state , 'accepted' ) ;
1801+
1802+ // Verify keychain creation was called
1803+ should . equal ( keychainsStub . calledOnce , true ) ;
1804+ // Verify encrypt was called with the correct parameters
1805+ should . equal ( encryptStub . calledWith ( { input : keychain . prv , password : userPassword } ) , true ) ;
1806+
1807+ walletShareNock . done ( ) ;
1808+ acceptShareNock . done ( ) ;
1809+ } ) ;
1810+
1811+ it ( 'should NOT trigger reshare when userMultiKeyRotationRequired is true' , async function ( ) {
1812+ const shareId = 'test_multi_key_3' ;
1813+ const userPassword = 'test_password_123' ;
1814+ const walletId = 'test_wallet_123' ;
1815+
1816+ const walletShareNock = nock ( bgUrl )
1817+ . get ( `/api/v2/tbtc/walletshare/${ shareId } ` )
1818+ . reply ( 200 , {
1819+ userMultiKeyRotationRequired : true ,
1820+ permissions : [ 'admin' , 'spend' , 'view' ] ,
1821+ wallet : walletId ,
1822+ } ) ;
1823+
1824+ const testKeychain = bitgo . keychains ( ) . create ( ) ;
1825+ const keychain = {
1826+ prv : testKeychain . xprv ,
1827+ pub : testKeychain . xpub ,
1828+ } ;
1829+ const encryptedPrv = bitgo . encrypt ( { input : keychain . prv , password : userPassword } ) ;
1830+
1831+ const acceptShareNock = nock ( bgUrl )
1832+ . post ( `/api/v2/tbtc/walletshare/${ shareId } ` )
1833+ . reply ( 200 , { changed : true , state : 'accepted' } ) ;
1834+
1835+ sandbox . stub ( wallets . baseCoin . keychains ( ) , 'create' ) . resolves ( keychain ) ;
1836+ sandbox . stub ( bitgo , 'encrypt' ) . returns ( encryptedPrv ) ;
1837+
1838+ // Stub reshareWalletWithSpenders to verify it's NOT called
1839+ const reshareStub = sandbox . stub ( Wallets . prototype , 'reshareWalletWithSpenders' ) ;
1840+
1841+ const res = await wallets . acceptShare ( { walletShareId : shareId , userPassword } ) ;
1842+ should . equal ( res . changed , true ) ;
1843+ should . equal ( res . state , 'accepted' ) ;
1844+
1845+ // Verify reshare was NOT called (unlike keychainOverrideRequired case)
1846+ should . equal ( reshareStub . called , false ) ;
1847+
1848+ walletShareNock . done ( ) ;
1849+ acceptShareNock . done ( ) ;
1850+ } ) ;
1851+
1852+ it ( 'should handle bulk accept share with userMultiKeyRotationRequired' , async function ( ) {
1853+ const shareId = 'test_multi_key_bulk_1' ;
1854+ const userPassword = 'test_password_123' ;
1855+
1856+ // Mock listSharesV2 to return a share with userMultiKeyRotationRequired
1857+ // Note: userMultiKeyRotationRequired shares don't have a keychain, so they won't be filtered
1858+ // by the bulkAcceptShare filter, but processAcceptShare will handle them
1859+ sinon . stub ( Wallets . prototype , 'listSharesV2' ) . resolves ( {
1860+ incoming : [
1861+ {
1862+ id : shareId ,
1863+ coin : 'tbtc' ,
1864+ walletLabel : 'test wallet' ,
1865+ fromUser : 'fromUser' ,
1866+ toUser : 'toUser' ,
1867+ wallet : 'wallet123' ,
1868+ permissions : [ 'admin' , 'spend' , 'view' ] ,
1869+ state : 'active' ,
1870+ userMultiKeyRotationRequired : true ,
1871+ // No keychain - this is the key difference for multi-user-key shares
1872+ } ,
1873+ ] ,
1874+ outgoing : [ ] ,
1875+ } ) ;
1876+
1877+ const testKeychain = bitgo . keychains ( ) . create ( ) ;
1878+ const keychain = {
1879+ prv : testKeychain . xprv ,
1880+ pub : testKeychain . xpub ,
1881+ } ;
1882+ const encryptedPrv = bitgo . encrypt ( { input : keychain . prv , password : userPassword } ) ;
1883+
1884+ // Mock bulk update API - this is called via bulkUpdateWalletShare
1885+ nock ( bgUrl )
1886+ . put ( '/api/v2/walletshares/update' , ( body : any ) => {
1887+ if ( ! body . shares || body . shares . length !== 1 ) {
1888+ return false ;
1889+ }
1890+ const share = body . shares [ 0 ] ;
1891+ return (
1892+ share . walletShareId === shareId &&
1893+ share . status === 'accept' &&
1894+ share . pub === keychain . pub &&
1895+ share . encryptedPrv === encryptedPrv
1896+ ) ;
1897+ } )
1898+ . reply ( 200 , {
1899+ acceptedWalletShares : [ { walletShareId : shareId } ] ,
1900+ rejectedWalletShares : [ ] ,
1901+ walletShareUpdateErrors : [ ] ,
1902+ } ) ;
1903+
1904+ const keychainsStub = sandbox . stub ( wallets . baseCoin . keychains ( ) , 'create' ) . resolves ( keychain ) ;
1905+ const encryptStub = sandbox . stub ( bitgo , 'encrypt' ) . returns ( encryptedPrv ) ;
1906+
1907+ // Note: bulkAcceptShare filters for shares WITH keychains, but userMultiKeyRotationRequired
1908+ // shares don't have keychains, so they go through a different path
1909+ // We need to stub the processAcceptShare path or use bulkUpdateWalletShare directly
1910+ // For this test, we'll use bulkUpdateWalletShare which calls processAcceptShare internally
1911+ const result = await wallets . bulkUpdateWalletShare ( {
1912+ shares : [ { walletShareId : shareId , status : 'accept' } ] ,
1913+ userLoginPassword : userPassword ,
1914+ } ) ;
1915+
1916+ should . equal ( result . acceptedWalletShares . length , 1 ) ;
1917+ should . deepEqual ( result . acceptedWalletShares [ 0 ] , { walletShareId : 'test_multi_key_bulk_1' } ) ;
1918+ should . equal ( keychainsStub . calledOnce , true ) ;
1919+ should . equal ( encryptStub . calledWith ( { input : keychain . prv , password : userPassword } ) , true ) ;
1920+ } ) ;
1921+ } ) ;
1922+
17311923 it ( 'should share a wallet to viewer' , async function ( ) {
17321924 const shareId = '12311' ;
17331925
0 commit comments