@@ -181,11 +181,9 @@ describe('signMultisigTransaction — external signing mode', () => {
181181 getFullName : ( ) => 'Test Bitcoin' ,
182182 } as unknown as BaseCoin ;
183183
184- coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( utxoCoinStub ) ;
185-
186- const nonUtxoCoinStub = {
187- getFamily : ( ) => CoinFamily . ETH ,
188- getFullName : ( ) => 'Test Ethereum' ,
184+ const unsupportedCoinStub = {
185+ getFamily : ( ) => CoinFamily . XRP ,
186+ getFullName : ( ) => 'Test XRP' ,
189187 } as unknown as BaseCoin ;
190188
191189 before ( ( ) => {
@@ -231,8 +229,8 @@ describe('signMultisigTransaction — external signing mode', () => {
231229 getKeyNock . isDone ( ) . should . equal ( false ) ;
232230 } ) ;
233231
234- it ( 'should fall through to local path for non-UTXO coin in external mode' , async ( ) => {
235- coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( nonUtxoCoinStub ) ;
232+ it ( 'should fall through to local path for unsupported coin in external mode' , async ( ) => {
233+ coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( unsupportedCoinStub ) ;
236234
237235 const signNock = nock ( keyProviderUrl ) . post ( '/sign' ) . reply ( 200 , { } ) ;
238236 nock ( keyProviderUrl ) . get ( `/key/${ userPub } ` ) . query ( { source : 'user' } ) . reply ( 200 , {
@@ -243,7 +241,7 @@ describe('signMultisigTransaction — external signing mode', () => {
243241 } ) ;
244242
245243 await agent
246- . post ( `/api/hteth /multisig/sign` )
244+ . post ( `/api/hxrp /multisig/sign` )
247245 . set ( 'Authorization' , `Bearer ${ accessToken } ` )
248246 . send ( { source : 'user' , pub : userPub , txPrebuild : { txHex } } ) ;
249247
@@ -261,4 +259,128 @@ describe('signMultisigTransaction — external signing mode', () => {
261259
262260 response . status . should . equal ( 500 ) ;
263261 } ) ;
262+
263+ describe ( 'ETH external signing' , ( ) => {
264+ const mockOperationHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890' ;
265+ const mockSignature =
266+ '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12' ;
267+ const mockExpireTime = 1735689600 ;
268+
269+ const ethCoinStub = {
270+ getFamily : ( ) => CoinFamily . ETH ,
271+ getFullName : ( ) => 'Test Ethereum' ,
272+ getDefaultExpireTime : sinon . stub ( ) . returns ( mockExpireTime ) ,
273+ getOperationSha3ForExecuteAndConfirm : sinon . stub ( ) . returns ( mockOperationHash ) ,
274+ } as unknown as BaseCoin ;
275+
276+ const txPrebuild = {
277+ recipients : [ { amount : '10000' , address : '0xe9cbfdf9e02f4ee37ec81683a4be934b4eecc295' } ] ,
278+ nextContractSequenceId : 5 ,
279+ gasLimit : 200000 ,
280+ eip1559 : { maxPriorityFeePerGas : '599413988' , maxFeePerGas : '23556954878' } ,
281+ isBatch : false ,
282+ } ;
283+
284+ it ( 'should call POST /sign with operationHash and return halfSigned' , async ( ) => {
285+ coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( ethCoinStub ) ;
286+
287+ const signNock = nock ( keyProviderUrl )
288+ . post ( '/sign' , {
289+ pub : userPub ,
290+ source : 'user' ,
291+ signablePayload : mockOperationHash ,
292+ algorithm : 'ecdsa' ,
293+ } )
294+ . reply ( 200 , { signature : mockSignature } ) ;
295+
296+ const getKeyNock = nock ( keyProviderUrl ) . get ( `/key/${ userPub } ` ) . reply ( 200 , { } ) ;
297+
298+ const response = await agent
299+ . post ( `/api/hteth/multisig/sign` )
300+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
301+ . send ( { source : 'user' , pub : userPub , txPrebuild } ) ;
302+
303+ response . status . should . equal ( 200 ) ;
304+ response . body . should . have . property ( 'halfSigned' ) ;
305+ response . body . halfSigned . should . have . property ( 'recipients' ) ;
306+ response . body . halfSigned . should . have . property ( 'expireTime' , mockExpireTime ) ;
307+ response . body . halfSigned . should . have . property (
308+ 'contractSequenceId' ,
309+ txPrebuild . nextContractSequenceId ,
310+ ) ;
311+ response . body . halfSigned . should . have . property ( 'operationHash' , mockOperationHash ) ;
312+ response . body . halfSigned . should . have . property ( 'signature' , mockSignature ) ;
313+ response . body . halfSigned . should . have . property ( 'isBatch' , txPrebuild . isBatch ) ;
314+
315+ signNock . done ( ) ;
316+
317+ /** Validate that the signing was done outside of the app: External Mode */
318+ getKeyNock . isDone ( ) . should . equal ( false ) ;
319+ } ) ;
320+
321+ it ( 'should return error when recipients missing from txPrebuild' , async ( ) => {
322+ coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( ethCoinStub ) ;
323+ const { recipients : __ , ...txPrebuildWithoutRecipients } = txPrebuild ;
324+
325+ const response = await agent
326+ . post ( `/api/hteth/multisig/sign` )
327+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
328+ . send ( { source : 'user' , pub : userPub , txPrebuild : txPrebuildWithoutRecipients } ) ;
329+
330+ response . status . should . equal ( 500 ) ;
331+ response . body . details . should . match ( / r e c i p i e n t s , n e x t C o n t r a c t S e q u e n c e I d / ) ;
332+ } ) ;
333+
334+ it ( 'should successfully sign when nextContractSequenceId is 0' , async ( ) => {
335+ coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( ethCoinStub ) ;
336+
337+ const txPrebuildWithZeroSequenceId = {
338+ ...txPrebuild ,
339+ nextContractSequenceId : 0 ,
340+ } ;
341+
342+ const signNock = nock ( keyProviderUrl )
343+ . post ( '/sign' , {
344+ pub : userPub ,
345+ source : 'user' ,
346+ signablePayload : mockOperationHash ,
347+ algorithm : 'ecdsa' ,
348+ } )
349+ . reply ( 200 , { signature : mockSignature } ) ;
350+
351+ const getKeyNock = nock ( keyProviderUrl ) . get ( `/key/${ userPub } ` ) . reply ( 200 , { } ) ;
352+
353+ const response = await agent
354+ . post ( `/api/hteth/multisig/sign` )
355+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
356+ . send ( { source : 'user' , pub : userPub , txPrebuild : txPrebuildWithZeroSequenceId } ) ;
357+
358+ response . status . should . equal ( 200 ) ;
359+ response . body . should . have . property ( 'halfSigned' ) ;
360+ response . body . halfSigned . should . have . property ( 'contractSequenceId' , 0 ) ;
361+
362+ signNock . done ( ) ;
363+ getKeyNock . isDone ( ) . should . equal ( false ) ;
364+ } ) ;
365+
366+ it ( 'should return error when keyProvider sign fails' , async ( ) => {
367+ coinStub = sinon . stub ( coinFactory , 'getCoin' ) . resolves ( ethCoinStub ) ;
368+
369+ nock ( keyProviderUrl )
370+ . post ( '/sign' , {
371+ pub : userPub ,
372+ source : 'user' ,
373+ signablePayload : mockOperationHash ,
374+ algorithm : 'ecdsa' ,
375+ } )
376+ . reply ( 500 , { error : 'KMS unavailable' } ) ;
377+
378+ const response = await agent
379+ . post ( `/api/hteth/multisig/sign` )
380+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
381+ . send ( { source : 'user' , pub : userPub , txPrebuild } ) ;
382+
383+ response . status . should . equal ( 500 ) ;
384+ } ) ;
385+ } ) ;
264386} ) ;
0 commit comments