@@ -65,9 +65,7 @@ vi.mock('@/components/wallet/address-display', () => ({
6565} ) ) ;
6666
6767vi . mock ( '@/components/common/amount-display' , ( ) => ( {
68- AmountDisplay : ( { value, symbol } : { value : string ; symbol : string } ) => (
69- < span > { `${ value } -${ symbol } ` } </ span >
70- ) ,
68+ AmountDisplay : ( { value, symbol } : { value : string ; symbol : string } ) => < span > { `${ value } -${ symbol } ` } </ span > ,
7169} ) ) ;
7270
7371vi . mock ( '@/components/wallet/chain-icon' , ( ) => ( {
@@ -89,11 +87,7 @@ vi.mock('@/components/security/pattern-lock', () => ({
8987 footerText ?: string ;
9088 } ) => (
9189 < div >
92- < button
93- type = "button"
94- data-testid = "pattern-lock"
95- onClick = { ( ) => onComplete ?.( [ 1 , 2 , 3 , 4 ] ) }
96- >
90+ < button type = "button" data-testid = "pattern-lock" onClick = { ( ) => onComplete ?.( [ 1 , 2 , 3 , 4 ] ) } >
9791 pattern
9892 </ button >
9993 < span data-testid = "pattern-lock-hint" > { hintText ?? '' } </ span >
@@ -135,7 +129,8 @@ vi.mock('@/components/security/password-input', () => ({
135129
136130vi . mock ( './miniapp-auth' , ( ) => ( {
137131 isMiniappWalletLockError : ( error : unknown ) => error instanceof Error && error . message . includes ( 'wallet lock' ) ,
138- isMiniappTwoStepSecretError : ( error : unknown ) => error instanceof Error && error . message . includes ( 'pay password' ) ,
132+ isMiniappTwoStepSecretError : ( error : unknown ) =>
133+ error instanceof Error && / ( p a y p a s s w o r d | s i g n \s * s i g n a t u r e (?: \s + i s ) ? \s + r e q u i r e d | 0 0 1 - 1 1 0 0 3 ) / i. test ( error . message ) ,
139134 resolveMiniappTwoStepSecretRequired : vi . fn ( async ( ) => false ) ,
140135} ) ) ;
141136
@@ -144,6 +139,7 @@ import { MiniappSignTransactionJob } from './MiniappSignTransactionJob';
144139import { signUnsignedTransaction } from '@/services/ecosystem/handlers' ;
145140import { getChainProvider } from '@/services/chain-adapter/providers' ;
146141import { superjson } from '@biochain/chain-effect' ;
142+ import { resolveMiniappTwoStepSecretRequired } from './miniapp-auth' ;
147143
148144describe ( 'miniapp confirm jobs regressions' , ( ) => {
149145 beforeEach ( ( ) => {
@@ -152,22 +148,26 @@ describe('miniapp confirm jobs regressions', () => {
152148 hoisted . currentParams = { } ;
153149
154150 vi . mocked ( getChainProvider ) . mockReset ( ) ;
155- vi . mocked ( getChainProvider ) . mockImplementation ( ( ) => ( {
156- supportsFullTransaction : true ,
157- buildTransaction : vi . fn ( async ( intent : unknown ) => ( {
158- chainId : 'bfmetav2' ,
159- intentType : 'transfer' ,
160- data : intent ,
161- } ) ) ,
162- signTransaction : vi . fn ( ) ,
163- broadcastTransaction : vi . fn ( async ( ) => {
164- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
165- return 'tx-hash' ;
166- } ) ,
167- } as unknown as ReturnType < typeof getChainProvider > ) ) ;
151+ vi . mocked ( getChainProvider ) . mockImplementation (
152+ ( ) =>
153+ ( {
154+ supportsFullTransaction : true ,
155+ buildTransaction : vi . fn ( async ( intent : unknown ) => ( {
156+ chainId : 'bfmetav2' ,
157+ intentType : 'transfer' ,
158+ data : intent ,
159+ } ) ) ,
160+ signTransaction : vi . fn ( ) ,
161+ broadcastTransaction : vi . fn ( async ( ) => {
162+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
163+ return 'tx-hash' ;
164+ } ) ,
165+ } ) as unknown as ReturnType < typeof getChainProvider > ,
166+ ) ;
168167
169168 vi . mocked ( signUnsignedTransaction ) . mockReset ( ) ;
170169 vi . mocked ( signUnsignedTransaction ) . mockResolvedValue ( { chainId : 'bfmetav2' , data : { tx : '1' } , signature : 'sig' } ) ;
170+ vi . mocked ( resolveMiniappTwoStepSecretRequired ) . mockResolvedValue ( false ) ;
171171
172172 walletStore . setState ( ( ) => ( {
173173 wallets : [
@@ -272,6 +272,48 @@ describe('miniapp confirm jobs regressions', () => {
272272 expect ( screen . getByTestId ( 'pattern-lock-footer' ) . textContent ) . toContain ( 'sign service timeout' ) ;
273273 } ) ;
274274
275+ it ( 'requires two-step secret before miniapp signing when account has second public key' , async ( ) => {
276+ vi . mocked ( resolveMiniappTwoStepSecretRequired ) . mockResolvedValueOnce ( true ) ;
277+
278+ render (
279+ < MiniappSignTransactionJob
280+ params = { {
281+ appName : 'Org App' ,
282+ appIcon : '' ,
283+ from : 'b_sender_1' ,
284+ chain : 'BFMetaV2' ,
285+ unsignedTx : superjson . stringify ( {
286+ chainId : 'bfmetav2' ,
287+ intentType : 'transfer' ,
288+ data : { tx : 'unsigned' } ,
289+ } ) ,
290+ } }
291+ /> ,
292+ ) ;
293+
294+ const signButton = screen . getByTestId ( 'miniapp-sign-review-confirm' ) ;
295+ await waitFor ( ( ) => {
296+ expect ( signButton ) . not . toBeDisabled ( ) ;
297+ } ) ;
298+
299+ fireEvent . click ( signButton ) ;
300+ fireEvent . click ( screen . getByTestId ( 'pattern-lock' ) ) ;
301+
302+ expect ( await screen . findByTestId ( 'password-input' ) ) . toBeInTheDocument ( ) ;
303+ expect ( vi . mocked ( signUnsignedTransaction ) ) . not . toHaveBeenCalled ( ) ;
304+
305+ fireEvent . change ( screen . getByTestId ( 'password-input' ) , { target : { value : '123456' } } ) ;
306+ fireEvent . click ( screen . getByTestId ( 'miniapp-sign-two-step-secret-confirm' ) ) ;
307+
308+ await waitFor ( ( ) => {
309+ expect ( vi . mocked ( signUnsignedTransaction ) ) . toHaveBeenCalledWith (
310+ expect . objectContaining ( {
311+ paySecret : '123456' ,
312+ } ) ,
313+ ) ;
314+ } ) ;
315+ } ) ;
316+
275317 it ( 'does not pass raw amount directly to display layer' , ( ) => {
276318 render (
277319 < MiniappTransferConfirmJob
@@ -392,6 +434,46 @@ describe('miniapp confirm jobs regressions', () => {
392434 expect ( intent . amount . toRawString ( ) ) . toBe ( '1000000000' ) ;
393435 } ) ;
394436
437+ it ( 'requires two-step secret before miniapp transfer signing when account has second public key' , async ( ) => {
438+ vi . mocked ( resolveMiniappTwoStepSecretRequired ) . mockResolvedValueOnce ( true ) ;
439+
440+ render (
441+ < MiniappTransferConfirmJob
442+ params = { {
443+ appName : 'Org App' ,
444+ appIcon : '' ,
445+ from : 'b_sender_1' ,
446+ to : 'b_receiver_1' ,
447+ amount : '1000' ,
448+ chain : 'BFMetaV2' ,
449+ asset : 'BFM' ,
450+ } }
451+ /> ,
452+ ) ;
453+
454+ const confirmButton = screen . getByTestId ( 'miniapp-transfer-review-confirm' ) ;
455+ await waitFor ( ( ) => {
456+ expect ( confirmButton ) . not . toBeDisabled ( ) ;
457+ } ) ;
458+
459+ fireEvent . click ( confirmButton ) ;
460+ fireEvent . click ( screen . getByTestId ( 'pattern-lock' ) ) ;
461+
462+ expect ( await screen . findByTestId ( 'password-input' ) ) . toBeInTheDocument ( ) ;
463+ expect ( vi . mocked ( signUnsignedTransaction ) ) . not . toHaveBeenCalled ( ) ;
464+
465+ fireEvent . change ( screen . getByTestId ( 'password-input' ) , { target : { value : '654321' } } ) ;
466+ fireEvent . click ( screen . getByTestId ( 'miniapp-transfer-two-step-secret-confirm' ) ) ;
467+
468+ await waitFor ( ( ) => {
469+ expect ( vi . mocked ( signUnsignedTransaction ) ) . toHaveBeenCalledWith (
470+ expect . objectContaining ( {
471+ paySecret : '654321' ,
472+ } ) ,
473+ ) ;
474+ } ) ;
475+ } ) ;
476+
395477 it ( 'passes remark into transaction intent and keeps it in emitted transaction' , async ( ) => {
396478 const buildTransaction = vi . fn ( async ( intent : unknown ) => ( {
397479 chainId : 'bfmetav2' ,
@@ -412,18 +494,20 @@ describe('miniapp confirm jobs regressions', () => {
412494 signature : 'sig' ,
413495 } ) ) ;
414496
415- const eventPromise = new Promise < CustomEvent < { confirmed ?: boolean ; transaction ?: Record < string , unknown > } > > ( ( resolve ) => {
416- const handleEvent = ( event : Event ) => {
417- const customEvent = event as CustomEvent < { confirmed ?: boolean ; transaction ?: Record < string , unknown > } > ;
418- if ( customEvent . detail ?. confirmed !== true ) {
419- return ;
420- }
421- window . removeEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
422- resolve ( customEvent ) ;
423- } ;
497+ const eventPromise = new Promise < CustomEvent < { confirmed ?: boolean ; transaction ?: Record < string , unknown > } > > (
498+ ( resolve ) => {
499+ const handleEvent = ( event : Event ) => {
500+ const customEvent = event as CustomEvent < { confirmed ?: boolean ; transaction ?: Record < string , unknown > } > ;
501+ if ( customEvent . detail ?. confirmed !== true ) {
502+ return ;
503+ }
504+ window . removeEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
505+ resolve ( customEvent ) ;
506+ } ;
424507
425- window . addEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
426- } ) ;
508+ window . addEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
509+ } ,
510+ ) ;
427511
428512 render (
429513 < MiniappTransferConfirmJob
@@ -565,7 +649,6 @@ describe('miniapp confirm jobs regressions', () => {
565649 expect ( broadcastTransaction ) . not . toHaveBeenCalled ( ) ;
566650 } ) ;
567651
568-
569652 it ( 'ignores duplicated unlock submission while transfer is in-flight' , async ( ) => {
570653 vi . mocked ( signUnsignedTransaction ) . mockImplementation ( async ( ) => {
571654 await new Promise ( ( resolve ) => setTimeout ( resolve , 120 ) ) ;
@@ -751,24 +834,27 @@ describe('miniapp confirm jobs regressions', () => {
751834 } ) ;
752835
753836 it ( 'uses toast and exits broadcasting state when background broadcast fails' , async ( ) => {
754- vi . mocked ( getChainProvider ) . mockImplementation ( ( ) => ( {
755- supportsFullTransaction : true ,
756- buildTransaction : vi . fn ( async ( intent : unknown ) => ( {
757- chainId : 'bfmetav2' ,
758- intentType : 'transfer' ,
759- data : intent ,
760- } ) ) ,
761- signTransaction : vi . fn ( ) ,
762- broadcastTransaction : vi . fn ( async ( ) => {
763- await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
764- throw new ChainServiceError (
765- ChainErrorCodes . TX_BROADCAST_FAILED ,
766- 'Failed to broadcast transaction' ,
767- undefined ,
768- new Error ( 'Request timeout' ) ,
769- ) ;
770- } ) ,
771- } as unknown as ReturnType < typeof getChainProvider > ) ) ;
837+ vi . mocked ( getChainProvider ) . mockImplementation (
838+ ( ) =>
839+ ( {
840+ supportsFullTransaction : true ,
841+ buildTransaction : vi . fn ( async ( intent : unknown ) => ( {
842+ chainId : 'bfmetav2' ,
843+ intentType : 'transfer' ,
844+ data : intent ,
845+ } ) ) ,
846+ signTransaction : vi . fn ( ) ,
847+ broadcastTransaction : vi . fn ( async ( ) => {
848+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
849+ throw new ChainServiceError (
850+ ChainErrorCodes . TX_BROADCAST_FAILED ,
851+ 'Failed to broadcast transaction' ,
852+ undefined ,
853+ new Error ( 'Request timeout' ) ,
854+ ) ;
855+ } ) ,
856+ } ) as unknown as ReturnType < typeof getChainProvider > ,
857+ ) ;
772858
773859 vi . mocked ( signUnsignedTransaction ) . mockResolvedValue ( { chainId : 'bfmetav2' , data : { tx : '1' } , signature : 'sig' } ) ;
774860
@@ -819,22 +905,23 @@ describe('miniapp confirm jobs regressions', () => {
819905 expect ( screen . queryByTestId ( 'miniapp-transfer-error' ) ) . not . toBeInTheDocument ( ) ;
820906 } ) ;
821907
822-
823908 it ( 'emits transfer result with the same requestId' , async ( ) => {
824909 const requestId = 'transfer-test-request-id' ;
825910
826- const eventPromise = new Promise < CustomEvent < { requestId ?: string ; confirmed ?: boolean ; txHash ?: string } > > ( ( resolve ) => {
827- const handleEvent = ( event : Event ) => {
828- const customEvent = event as CustomEvent < { requestId ?: string ; confirmed ?: boolean ; txHash ?: string } > ;
829- if ( customEvent . detail ?. requestId !== requestId ) {
830- return ;
831- }
832- window . removeEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
833- resolve ( customEvent ) ;
834- } ;
911+ const eventPromise = new Promise < CustomEvent < { requestId ?: string ; confirmed ?: boolean ; txHash ?: string } > > (
912+ ( resolve ) => {
913+ const handleEvent = ( event : Event ) => {
914+ const customEvent = event as CustomEvent < { requestId ?: string ; confirmed ?: boolean ; txHash ?: string } > ;
915+ if ( customEvent . detail ?. requestId !== requestId ) {
916+ return ;
917+ }
918+ window . removeEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
919+ resolve ( customEvent ) ;
920+ } ;
835921
836- window . addEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
837- } ) ;
922+ window . addEventListener ( 'miniapp-transfer-confirm' , handleEvent ) ;
923+ } ,
924+ ) ;
838925
839926 render (
840927 < MiniappTransferConfirmJob
@@ -883,27 +970,32 @@ describe('miniapp confirm jobs regressions', () => {
883970 } ;
884971 } ) ;
885972
886- vi . mocked ( getChainProvider ) . mockImplementation ( ( ) => ( {
887- supportsFullTransaction : true ,
888- buildTransaction : vi . fn ( async ( intent : unknown ) => ( {
889- chainId : 'bfmetav2' ,
890- intentType : 'transfer' ,
891- data : intent ,
892- } ) ) ,
893- signTransaction : vi . fn ( ) ,
894- broadcastTransaction : vi . fn ( async ( signedTx : { data : unknown } ) => {
895- const payload = signedTx . data as { signature ?: string } ;
896- return 'tx-hash-' + ( payload . signature ?? 'unknown' ) ;
897- } ) ,
898- } as unknown as ReturnType < typeof getChainProvider > ) ) ;
973+ vi . mocked ( getChainProvider ) . mockImplementation (
974+ ( ) =>
975+ ( {
976+ supportsFullTransaction : true ,
977+ buildTransaction : vi . fn ( async ( intent : unknown ) => ( {
978+ chainId : 'bfmetav2' ,
979+ intentType : 'transfer' ,
980+ data : intent ,
981+ } ) ) ,
982+ signTransaction : vi . fn ( ) ,
983+ broadcastTransaction : vi . fn ( async ( signedTx : { data : unknown } ) => {
984+ const payload = signedTx . data as { signature ?: string } ;
985+ return 'tx-hash-' + ( payload . signature ?? 'unknown' ) ;
986+ } ) ,
987+ } ) as unknown as ReturnType < typeof getChainProvider > ,
988+ ) ;
899989
900990 const runTransfer = async ( requestId : string ) => {
901- const eventPromise = new Promise < CustomEvent < {
902- requestId ?: string ;
903- confirmed ?: boolean ;
904- txHash ?: string ;
905- transaction ?: Record < string , unknown > ;
906- } > > ( ( resolve ) => {
991+ const eventPromise = new Promise <
992+ CustomEvent < {
993+ requestId ?: string ;
994+ confirmed ?: boolean ;
995+ txHash ?: string ;
996+ transaction ?: Record < string , unknown > ;
997+ } >
998+ > ( ( resolve ) => {
907999 const handleEvent = ( event : Event ) => {
9081000 const customEvent = event as CustomEvent < {
9091001 requestId ?: string ;
0 commit comments