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