@@ -305,6 +305,166 @@ describe('Verify Transaction', function () {
305305 bitcoinMock . restore ( ) ;
306306 } ) ;
307307
308+ it ( 'should verify a bridging transaction whose implicit external output matches the bridge amount' , async ( ) => {
309+ // Bridging intents (e.g. BTC -> sBTC peg-in) carry no recipients; the single external output
310+ // is the bridge deposit address computed server-side. With explicitExternalSpendAmount 0 the
311+ // paygo limit is 0, so any implicit external output would normally be rejected as an
312+ // "unintended external recipient". For type: 'bridging' we instead verify that the implicit
313+ // external spend equals bridgingParams.sbtc.amount.
314+ const coinMock = sinon . stub ( coin , 'parseTransaction' ) . resolves ( {
315+ keychains : { } as any ,
316+ keySignatures : { } ,
317+ outputs : [ ] ,
318+ missingOutputs : [ ] ,
319+ explicitExternalOutputs : [ ] ,
320+ implicitExternalOutputs : [
321+ {
322+ address : 'sbtc_deposit_address' ,
323+ amount : '22000' ,
324+ } ,
325+ ] ,
326+ changeOutputs : [ ] ,
327+ explicitExternalSpendAmount : 0 ,
328+ implicitExternalSpendAmount : 22000 ,
329+ needsCustomChangeKeySignatureVerification : false ,
330+ } ) ;
331+
332+ const bitcoinMock = sinon
333+ . stub ( coin , 'createTransactionFromHex' )
334+ . returns ( { ins : [ ] } as unknown as utxolib . bitgo . UtxoTransaction ) ;
335+
336+ const result = await coin . verifyTransaction ( {
337+ txParams : {
338+ walletPassphrase : passphrase ,
339+ type : 'bridging' ,
340+ bridgingParams : { sbtc : { amount : '22000' , stacksRecipient : 'SM1X' , maxFee : '1000' , lockTime : 100 } } ,
341+ } ,
342+ txPrebuild : {
343+ txHex : '00' ,
344+ } ,
345+ wallet : unsignedSendingWallet as any ,
346+ verification : { } ,
347+ } ) ;
348+
349+ assert . strictEqual ( result , true ) ;
350+
351+ coinMock . restore ( ) ;
352+ bitcoinMock . restore ( ) ;
353+ } ) ;
354+
355+ it ( 'should reject a bridging transaction whose implicit external output does not match the bridge amount' , async ( ) => {
356+ const coinMock = sinon . stub ( coin , 'parseTransaction' ) . resolves ( {
357+ keychains : { } as any ,
358+ keySignatures : { } ,
359+ outputs : [ ] ,
360+ missingOutputs : [ ] ,
361+ explicitExternalOutputs : [ ] ,
362+ implicitExternalOutputs : [
363+ {
364+ address : 'sbtc_deposit_address' ,
365+ amount : '50000' ,
366+ } ,
367+ ] ,
368+ changeOutputs : [ ] ,
369+ explicitExternalSpendAmount : 0 ,
370+ implicitExternalSpendAmount : 50000 ,
371+ needsCustomChangeKeySignatureVerification : false ,
372+ } ) ;
373+
374+ await assert . rejects (
375+ coin . verifyTransaction ( {
376+ txParams : {
377+ walletPassphrase : passphrase ,
378+ type : 'bridging' ,
379+ bridgingParams : { sbtc : { amount : '22000' , stacksRecipient : 'SM1X' , maxFee : '1000' , lockTime : 100 } } ,
380+ } ,
381+ txPrebuild : {
382+ txHex : '00' ,
383+ } ,
384+ wallet : unsignedSendingWallet as any ,
385+ verification : { } ,
386+ } ) ,
387+ / b r i d g i n g o u t p u t a m o u n t \( 5 0 0 0 0 \) d o e s n o t m a t c h i n t e n d e d b r i d g e a m o u n t \( 2 2 0 0 0 \) /
388+ ) ;
389+
390+ coinMock . restore ( ) ;
391+ } ) ;
392+
393+ it ( 'should reject a bridging transaction that is missing bridgingParams.sbtc.amount' , async ( ) => {
394+ const coinMock = sinon . stub ( coin , 'parseTransaction' ) . resolves ( {
395+ keychains : { } as any ,
396+ keySignatures : { } ,
397+ outputs : [ ] ,
398+ missingOutputs : [ ] ,
399+ explicitExternalOutputs : [ ] ,
400+ implicitExternalOutputs : [
401+ {
402+ address : 'sbtc_deposit_address' ,
403+ amount : '22000' ,
404+ } ,
405+ ] ,
406+ changeOutputs : [ ] ,
407+ explicitExternalSpendAmount : 0 ,
408+ implicitExternalSpendAmount : 22000 ,
409+ needsCustomChangeKeySignatureVerification : false ,
410+ } ) ;
411+
412+ await assert . rejects (
413+ coin . verifyTransaction ( {
414+ txParams : {
415+ walletPassphrase : passphrase ,
416+ type : 'bridging' ,
417+ } ,
418+ txPrebuild : {
419+ txHex : '00' ,
420+ } ,
421+ wallet : unsignedSendingWallet as any ,
422+ verification : { } ,
423+ } ) ,
424+ / b r i d g i n g t r a n s a c t i o n i s m i s s i n g b r i d g i n g P a r a m s .s b t c .a m o u n t /
425+ ) ;
426+
427+ coinMock . restore ( ) ;
428+ } ) ;
429+
430+ it ( 'should still reject the same implicit external output when the intent is not bridging' , async ( ) => {
431+ // Same shape as the bridging test above, but without type: 'bridging' the implicit external
432+ // output is treated as an unintended external recipient and rejected.
433+ const coinMock = sinon . stub ( coin , 'parseTransaction' ) . resolves ( {
434+ keychains : { } as any ,
435+ keySignatures : { } ,
436+ outputs : [ ] ,
437+ missingOutputs : [ ] ,
438+ explicitExternalOutputs : [ ] ,
439+ implicitExternalOutputs : [
440+ {
441+ address : 'sbtc_deposit_address' ,
442+ amount : '22000' ,
443+ } ,
444+ ] ,
445+ changeOutputs : [ ] ,
446+ explicitExternalSpendAmount : 0 ,
447+ implicitExternalSpendAmount : 22000 ,
448+ needsCustomChangeKeySignatureVerification : false ,
449+ } ) ;
450+
451+ await assert . rejects (
452+ coin . verifyTransaction ( {
453+ txParams : {
454+ walletPassphrase : passphrase ,
455+ } ,
456+ txPrebuild : {
457+ txHex : '00' ,
458+ } ,
459+ wallet : unsignedSendingWallet as any ,
460+ verification : { } ,
461+ } ) ,
462+ / p r e b u i l d a t t e m p t s t o s p e n d t o u n i n t e n d e d e x t e r n a l r e c i p i e n t s /
463+ ) ;
464+
465+ coinMock . restore ( ) ;
466+ } ) ;
467+
308468 it ( 'should work with bigint amounts' , async ( ) => {
309469 // need a coin that uses bigint
310470 const bigintCoin = getUtxoCoin ( 'tdoge' ) ;
0 commit comments