Skip to content

Commit c1591e1

Browse files
authored
feat: complete implementation (#500)
1 parent b0316d9 commit c1591e1

7 files changed

Lines changed: 345 additions & 165 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 更新日志
22

3+
## [1.0.20] - 2026-03-01
4+
5+
fix miniapp 二次密码签名流程并补全 signSignature required 识别
6+
7+
<!-- last-commit: pending -->
8+
39
## [1.0.19] - 2026-03-01
410

511
修复传送门比例语义回退,与后端配置一致

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@biochain/keyapp",
33
"private": true,
4-
"version": "1.0.19",
4+
"version": "1.0.20",
55
"type": "module",
66
"packageManager": "pnpm@10.28.0",
77
"scripts": {

src/stackflow/activities/sheets/MiniappConfirmJobs.regression.test.tsx

Lines changed: 175 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,7 @@ vi.mock('@/components/wallet/address-display', () => ({
6565
}));
6666

6767
vi.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

7371
vi.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

136130
vi.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 && /(pay password|sign\s*signature(?:\s+is)?\s+required|001-11003)/i.test(error.message),
139134
resolveMiniappTwoStepSecretRequired: vi.fn(async () => false),
140135
}));
141136

@@ -144,6 +139,7 @@ import { MiniappSignTransactionJob } from './MiniappSignTransactionJob';
144139
import { signUnsignedTransaction } from '@/services/ecosystem/handlers';
145140
import { getChainProvider } from '@/services/chain-adapter/providers';
146141
import { superjson } from '@biochain/chain-effect';
142+
import { resolveMiniappTwoStepSecretRequired } from './miniapp-auth';
147143

148144
describe('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

Comments
 (0)