@@ -1835,3 +1835,182 @@ describe('signRecoveryEddsaMPCv2', () => {
18351835 ) ;
18361836 } ) ;
18371837} ) ;
1838+
1839+ describe ( 'EdDSA MPCv2 signRequestBase recipient verification' , ( ) => {
1840+ let eddsaMPCv2Utils : EddsaMPCv2Utils ;
1841+ let verifyTransactionStub : sinon . SinonStub ;
1842+ let userKeyShare : Buffer ;
1843+
1844+ const walletId = 'wallet-verify-test' ;
1845+ const signableHex = 'deadbeef' ;
1846+ const serializedTxHex = 'cafebabe' ;
1847+ const derivationPath = 'm/0' ;
1848+
1849+ before ( async ( ) => {
1850+ const [ userDkg ] = await MPSUtil . generateEdDsaDKGKeyShares ( ) ;
1851+ userKeyShare = userDkg . getKeyShare ( ) ;
1852+ } ) ;
1853+
1854+ beforeEach ( async ( ) => {
1855+ verifyTransactionStub = sinon . stub ( ) . resolves ( true ) ;
1856+
1857+ const mockBitgo = {
1858+ getEnv : sinon . stub ( ) . returns ( 'test' ) ,
1859+ setRequestTracer : sinon . stub ( ) ,
1860+ url : sinon . stub ( ) . callsFake ( ( path : string ) => `https://test.bitgo.com${ path } ` ) ,
1861+ post : sinon . stub ( ) . returns ( {
1862+ send : sinon . stub ( ) . returnsThis ( ) ,
1863+ set : sinon . stub ( ) . returnsThis ( ) ,
1864+ result : sinon . stub ( ) . rejects ( new Error ( 'mock: HTTP not available' ) ) ,
1865+ } ) ,
1866+ } as unknown as BitGoBase ;
1867+
1868+ const mockCoin = {
1869+ getMPCAlgorithm : sinon . stub ( ) . returns ( 'eddsa' ) ,
1870+ verifyTransaction : verifyTransactionStub ,
1871+ } as unknown as IBaseCoin ;
1872+
1873+ const mockWallet = {
1874+ id : sinon . stub ( ) . returns ( walletId ) ,
1875+ multisigType : sinon . stub ( ) . returns ( 'tss' ) ,
1876+ multisigTypeVersion : sinon . stub ( ) . returns ( 'MPCv2' ) ,
1877+ } as unknown as IWallet ;
1878+
1879+ eddsaMPCv2Utils = new EddsaMPCv2Utils ( mockBitgo , mockCoin , mockWallet ) ;
1880+ sinon
1881+ . stub ( eddsaMPCv2Utils as any , 'pickBitgoPubGpgKeyForSigning' )
1882+ . resolves ( await pgp . readKey ( { armoredKey : ( await generateGPGKeyPair ( 'ed25519' ) ) . publicKey } ) ) ;
1883+ } ) ;
1884+
1885+ afterEach ( ( ) => {
1886+ sinon . restore ( ) ;
1887+ } ) ;
1888+
1889+ it ( 'should call verifyTransaction with resolveEffectiveTxParams output' , async ( ) => {
1890+ const txRequest : TxRequest = {
1891+ txRequestId : 'txreq-verify-1' ,
1892+ walletId,
1893+ apiVersion : 'full' ,
1894+ transactions : [
1895+ {
1896+ unsignedTx : { signableHex, serializedTxHex, derivationPath } ,
1897+ signatureShares : [ ] ,
1898+ } ,
1899+ ] ,
1900+ intent : {
1901+ intentType : 'payment' ,
1902+ recipients : [ { address : { address : 'solAddr1' } , amount : { value : '5000000' , symbol : 'tsol' } } ] ,
1903+ } ,
1904+ unsignedTxs : [ ] ,
1905+ } as unknown as TxRequest ;
1906+
1907+ try {
1908+ await eddsaMPCv2Utils . signTxRequest ( {
1909+ txRequest,
1910+ txParams : { recipients : [ { address : 'solAddr1' , amount : '5000000' } ] } ,
1911+ prv : userKeyShare . toString ( 'base64' ) ,
1912+ reqId : new RequestTracer ( ) ,
1913+ } ) ;
1914+ } catch {
1915+ // Expected to fail at MPC signing rounds — we only care about verifyTransaction
1916+ }
1917+
1918+ sinon . assert . calledOnce ( verifyTransactionStub ) ;
1919+ const call = verifyTransactionStub . getCall ( 0 ) ;
1920+ assert . strictEqual ( call . args [ 0 ] . txPrebuild . txHex , serializedTxHex ) ;
1921+ assert . deepStrictEqual ( call . args [ 0 ] . txParams . recipients , [ { address : 'solAddr1' , amount : '5000000' } ] ) ;
1922+ } ) ;
1923+
1924+ it ( 'should resolve recipients from intent when txParams has none' , async ( ) => {
1925+ const txRequest : TxRequest = {
1926+ txRequestId : 'txreq-verify-2' ,
1927+ walletId,
1928+ apiVersion : 'full' ,
1929+ transactions : [
1930+ {
1931+ unsignedTx : { signableHex, serializedTxHex, derivationPath } ,
1932+ signatureShares : [ ] ,
1933+ } ,
1934+ ] ,
1935+ intent : {
1936+ intentType : 'payment' ,
1937+ recipients : [ { address : { address : 'solAddr2' } , amount : { value : '1000' , symbol : 'tsol' } } ] ,
1938+ } ,
1939+ unsignedTxs : [ ] ,
1940+ } as unknown as TxRequest ;
1941+
1942+ try {
1943+ await eddsaMPCv2Utils . signTxRequest ( {
1944+ txRequest,
1945+ prv : userKeyShare . toString ( 'base64' ) ,
1946+ reqId : new RequestTracer ( ) ,
1947+ } ) ;
1948+ } catch {
1949+ // Expected to fail at MPC signing rounds
1950+ }
1951+
1952+ sinon . assert . calledOnce ( verifyTransactionStub ) ;
1953+ const call = verifyTransactionStub . getCall ( 0 ) ;
1954+ assert . strictEqual ( call . args [ 0 ] . txParams . recipients [ 0 ] . address , 'solAddr2' ) ;
1955+ assert . strictEqual ( call . args [ 0 ] . txParams . recipients [ 0 ] . amount , '1000' ) ;
1956+ } ) ;
1957+
1958+ it ( 'should not call verifyTransaction for message signing' , async ( ) => {
1959+ const txRequest : TxRequest = {
1960+ txRequestId : 'txreq-verify-msg' ,
1961+ walletId,
1962+ apiVersion : 'full' ,
1963+ messages : [
1964+ {
1965+ messageEncoded : 'deadbeef' ,
1966+ derivationPath : 'm/0' ,
1967+ } ,
1968+ ] ,
1969+ unsignedTxs : [ ] ,
1970+ } as unknown as TxRequest ;
1971+
1972+ try {
1973+ await eddsaMPCv2Utils . signTxRequestForMessage ( {
1974+ txRequest,
1975+ prv : userKeyShare . toString ( 'base64' ) ,
1976+ reqId : new RequestTracer ( ) ,
1977+ messageRaw : 'test message' ,
1978+ bufferToSign : Buffer . from ( 'deadbeef' , 'hex' ) ,
1979+ } ) ;
1980+ } catch {
1981+ // Expected to fail at MPC signing rounds
1982+ }
1983+
1984+ sinon . assert . notCalled ( verifyTransactionStub ) ;
1985+ } ) ;
1986+
1987+ it ( 'should use signableHex as fallback when serializedTxHex is missing' , async ( ) => {
1988+ const txRequest : TxRequest = {
1989+ txRequestId : 'txreq-verify-fallback' ,
1990+ walletId,
1991+ apiVersion : 'full' ,
1992+ transactions : [
1993+ {
1994+ unsignedTx : { signableHex, derivationPath } ,
1995+ signatureShares : [ ] ,
1996+ } ,
1997+ ] ,
1998+ intent : { intentType : 'consolidate' } ,
1999+ unsignedTxs : [ ] ,
2000+ } as unknown as TxRequest ;
2001+
2002+ try {
2003+ await eddsaMPCv2Utils . signTxRequest ( {
2004+ txRequest,
2005+ prv : userKeyShare . toString ( 'base64' ) ,
2006+ reqId : new RequestTracer ( ) ,
2007+ } ) ;
2008+ } catch {
2009+ // Expected to fail at MPC signing rounds
2010+ }
2011+
2012+ sinon . assert . calledOnce ( verifyTransactionStub ) ;
2013+ const call = verifyTransactionStub . getCall ( 0 ) ;
2014+ assert . strictEqual ( call . args [ 0 ] . txPrebuild . txHex , signableHex ) ;
2015+ } ) ;
2016+ } ) ;
0 commit comments