diff --git a/packages/kit/src/views/Developer/pages/Gallery/Components/stories/BatchPsbtSigning.tsx b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/BatchPsbtSigning.tsx
new file mode 100644
index 000000000000..d297680e35ce
--- /dev/null
+++ b/packages/kit/src/views/Developer/pages/Gallery/Components/stories/BatchPsbtSigning.tsx
@@ -0,0 +1,1020 @@
+import { useEffect, useMemo, useState } from 'react';
+
+import { type RouteProp, useRoute } from '@react-navigation/core';
+import {
+ address as BitcoinJsAddress,
+ networks as BitcoinJsNetworks,
+ Psbt,
+ Transaction,
+ payments,
+} from 'bitcoinjs-lib';
+
+// cspell:ignore babylonlabs
+
+import {
+ Dialog,
+ Icon,
+ Page,
+ Progress,
+ SizableText,
+ Stack,
+ Toast,
+ XStack,
+ YStack,
+} from '@onekeyhq/components';
+import type { IEncodedTxBtc } from '@onekeyhq/core/src/chains/btc/types';
+import type { IUnsignedTxPro } from '@onekeyhq/core/src/types';
+import { AccountSelectorProviderMirror } from '@onekeyhq/kit/src/components/AccountSelector';
+import useAppNavigation from '@onekeyhq/kit/src/hooks/useAppNavigation';
+import { useActiveAccount } from '@onekeyhq/kit/src/states/jotai/contexts/accountSelector';
+import { OneKeyLocalError } from '@onekeyhq/shared/src/errors';
+import {
+ EModalRoutes,
+ EModalSignatureConfirmRoutes,
+} from '@onekeyhq/shared/src/routes';
+import type {
+ IBatchPsbtWalletType,
+ IModalSignatureConfirmParamList,
+} from '@onekeyhq/shared/src/routes';
+import { EAccountSelectorSceneName } from '@onekeyhq/shared/types';
+
+// Core functions are loaded dynamically to avoid a kit -> core value import.
+async function loadCoreBtcSdk() {
+ return import('@onekeyhq/core/src/chains/btc/sdkBtc');
+}
+
+type IMockTransaction = {
+ id: number;
+ amount: string;
+ fiatAmount: string;
+ recipient: string;
+ fee: string;
+ inputs: number;
+ outputs: number;
+};
+
+type TBatchSigningStage = 'overview' | 'signing' | 'complete';
+
+const MOCK_TRANSACTIONS: IMockTransaction[] = [
+ {
+ id: 1,
+ amount: '0.021 BTC',
+ fiatAmount: '$2,432.84',
+ recipient: 'bc1p8m…6q4x',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 2,
+ amount: '0.018 BTC',
+ fiatAmount: '$2,085.29',
+ recipient: 'bc1p0s…c83n',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 3,
+ amount: '0.026 BTC',
+ fiatAmount: '$3,012.08',
+ recipient: 'bc1p3d…9p2k',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 4,
+ amount: '0.015 BTC',
+ fiatAmount: '$1,737.74',
+ recipient: 'bc1ph7…qf8w',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 5,
+ amount: '0.019 BTC',
+ fiatAmount: '$2,201.14',
+ recipient: 'bc1p5k…v4zr',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 6,
+ amount: '0.017 BTC',
+ fiatAmount: '$1,969.86',
+ recipient: 'bc1p2n…8t6m',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 7,
+ amount: '0.024 BTC',
+ fiatAmount: '$2,781.58',
+ recipient: 'bc1px4…cw5j',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 8,
+ amount: '0.016 BTC',
+ fiatAmount: '$1,853.59',
+ recipient: 'bc1p9v…3h7s',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 9,
+ amount: '0.022 BTC',
+ fiatAmount: '$2,548.58',
+ recipient: 'bc1pj6…a2ne',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 10,
+ amount: '0.020 BTC',
+ fiatAmount: '$2,316.89',
+ recipient: 'bc1pr1…7z9d',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 11,
+ amount: '0.014 BTC',
+ fiatAmount: '$1,621.82',
+ recipient: 'bc1pf8…2u6c',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 12,
+ amount: '0.023 BTC',
+ fiatAmount: '$2,664.97',
+ recipient: 'bc1pm3…5w1q',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 13,
+ amount: '0.012 BTC',
+ fiatAmount: '$1,390.79',
+ recipient: 'bc1pt7…4b9x',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 14,
+ amount: '0.025 BTC',
+ fiatAmount: '$2,896.99',
+ recipient: 'bc1pc5…8r2v',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+ {
+ id: 15,
+ amount: '0.012 BTC',
+ fiatAmount: '$1,390.79',
+ recipient: 'bc1pn9…6k3f',
+ fee: '0.000012 BTC',
+ inputs: 1,
+ outputs: 2,
+ },
+];
+
+function getMockBitcoinNetwork(accountAddress: string) {
+ if (
+ accountAddress.startsWith('bc1') ||
+ accountAddress.startsWith('1') ||
+ accountAddress.startsWith('3')
+ ) {
+ return BitcoinJsNetworks.bitcoin;
+ }
+
+ return BitcoinJsNetworks.testnet;
+}
+
+function getAccountFullPath({
+ accountPath,
+ accountRelPath,
+}: {
+ accountPath: string;
+ accountRelPath?: string;
+}) {
+ const relPath = accountRelPath || '0/0';
+ return accountPath.endsWith(relPath)
+ ? accountPath
+ : `${accountPath}/${relPath}`;
+}
+
+function buildAccountMatchedMockPsbt({
+ transaction,
+ accountAddress,
+ accountPublicKey,
+}: {
+ transaction: IMockTransaction;
+ accountAddress: string;
+ accountPublicKey: string;
+}) {
+ const network = getMockBitcoinNetwork(accountAddress);
+ const publicKey = Buffer.from(accountPublicKey, 'hex');
+ const accountOutputScript = BitcoinJsAddress.toOutputScript(
+ accountAddress,
+ network,
+ );
+ const amountValue = Math.round(
+ Number(transaction.amount.replace(' BTC', '')) * 100_000_000,
+ );
+ const feeValue = Math.round(
+ Number(transaction.fee.replace(' BTC', '')) * 100_000_000,
+ );
+ const changeValue = 100_000;
+ const inputValue = amountValue + feeValue + changeValue;
+ const psbt = new Psbt({ network });
+ const inputHash = transaction.id.toString(16).padStart(64, '0');
+ const input = {
+ hash: inputHash,
+ index: 0,
+ sequence: 0xff_ff_ff_fd,
+ };
+
+ if (accountAddress.startsWith('1')) {
+ const fundingTransaction = new Transaction();
+ fundingTransaction.version = 2;
+ fundingTransaction.addInput(
+ Buffer.alloc(32, transaction.id),
+ 0xff_ff_ff_ff,
+ );
+ fundingTransaction.addOutput(accountOutputScript, BigInt(inputValue));
+ psbt.addInput({
+ ...input,
+ hash: fundingTransaction.getId(),
+ nonWitnessUtxo: fundingTransaction.toBuffer(),
+ });
+ } else if (accountAddress.startsWith('3') || accountAddress.startsWith('2')) {
+ const redeemPayment = payments.p2wpkh({ pubkey: publicKey, network });
+ const nestedSegwitPayment = payments.p2sh({
+ redeem: redeemPayment,
+ network,
+ });
+ if (
+ !redeemPayment.output ||
+ nestedSegwitPayment.address !== accountAddress
+ ) {
+ throw new OneKeyLocalError('Unable to create the mock redeem script');
+ }
+
+ psbt.addInput({
+ ...input,
+ witnessUtxo: {
+ script: accountOutputScript,
+ value: BigInt(inputValue),
+ },
+ redeemScript: redeemPayment.output,
+ });
+ } else if (
+ accountAddress.startsWith('bc1p') ||
+ accountAddress.startsWith('tb1p') ||
+ accountAddress.startsWith('bcrt1p')
+ ) {
+ psbt.addInput({
+ ...input,
+ witnessUtxo: {
+ script: accountOutputScript,
+ value: BigInt(inputValue),
+ },
+ tapInternalKey: publicKey.subarray(1, 33),
+ });
+ } else {
+ psbt.addInput({
+ ...input,
+ witnessUtxo: {
+ script: accountOutputScript,
+ value: BigInt(inputValue),
+ },
+ });
+ }
+
+ const recipientHash = Buffer.alloc(20, transaction.id);
+ const recipientScript = Buffer.concat([
+ Buffer.from([0x00, 0x14]),
+ recipientHash,
+ ]);
+ const recipientAddress = BitcoinJsAddress.fromOutputScript(
+ recipientScript,
+ network,
+ );
+
+ psbt.addOutput({
+ script: recipientScript,
+ value: BigInt(amountValue),
+ });
+ psbt.addOutput({
+ address: accountAddress,
+ value: BigInt(changeValue),
+ });
+
+ return {
+ amountValue,
+ changeValue,
+ feeValue,
+ inputHash: psbt.txInputs[0].hash,
+ inputValue,
+ psbtHex: psbt.toHex(),
+ recipientAddress,
+ };
+}
+
+function buildMockUnsignedTx({
+ transaction,
+ accountId,
+ networkId,
+ accountAddress,
+ accountPublicKey,
+ accountFullPath,
+}: {
+ transaction: IMockTransaction;
+ accountId: string;
+ networkId: string;
+ accountAddress: string;
+ accountPublicKey: string;
+ accountFullPath: string;
+}): IUnsignedTxPro {
+ const {
+ amountValue,
+ changeValue,
+ feeValue,
+ inputHash,
+ inputValue,
+ psbtHex,
+ recipientAddress,
+ } = buildAccountMatchedMockPsbt({
+ transaction,
+ accountAddress,
+ accountPublicKey,
+ });
+ const inputHashBuffer = Buffer.from(inputHash);
+ inputHashBuffer.reverse();
+ const inputTxid = inputHashBuffer.toString('hex');
+
+ const encodedTx: IEncodedTxBtc = {
+ inputs: [
+ {
+ txid: inputTxid,
+ vout: 0,
+ value: String(inputValue),
+ address: accountAddress,
+ path: accountFullPath,
+ },
+ ],
+ outputs: [
+ {
+ address: recipientAddress,
+ value: String(amountValue),
+ payload: { isChange: false },
+ },
+ {
+ address: accountAddress,
+ value: String(changeValue),
+ payload: { isChange: true },
+ },
+ ],
+ inputsForCoinSelect: [],
+ outputsForCoinSelect: [],
+ fee: String(feeValue),
+ psbtHex,
+ inputsToSign: [
+ {
+ index: 0,
+ publicKey: accountPublicKey,
+ address: accountAddress,
+ useTweakedSigner:
+ accountAddress.startsWith('bc1p') ||
+ accountAddress.startsWith('tb1p') ||
+ accountAddress.startsWith('bcrt1p'),
+ },
+ ],
+ disabledCoinSelect: true,
+ txSize: 180,
+ };
+
+ return {
+ uuid: `batch-psbt-preview-${transaction.id}`,
+ accountId,
+ networkId,
+ encodedTx,
+ };
+}
+
+function SummaryRow({ label, value }: { label: string; value: string }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+}
+
+function TransactionRow({
+ transaction,
+ signed,
+ onPress,
+}: {
+ transaction: IMockTransaction;
+ signed: boolean;
+ onPress: () => void;
+}) {
+ return (
+
+
+ {signed ? (
+
+ ) : (
+
+ {String(transaction.id).padStart(2, '0')}
+
+ )}
+
+
+
+
+
+ {`Transaction ${transaction.id}`}
+
+ {signed ? (
+
+ Signed
+
+ ) : null}
+
+
+ {`To ${transaction.recipient}`}
+
+
+
+
+
+ {`−${transaction.amount}`}
+
+
+ {transaction.fiatAmount}
+
+
+
+
+
+ );
+}
+
+function BatchSigningProgress({
+ signedCount,
+ currentTransaction,
+}: {
+ signedCount: number;
+ currentTransaction?: IMockTransaction;
+}) {
+ const totalCount = MOCK_TRANSACTIONS.length;
+ const remainingCount = totalCount - signedCount;
+ const isComplete = remainingCount === 0;
+ const progressValue = (signedCount / totalCount) * 100;
+ let progressDescription =
+ 'Review and approve this transaction on your hardware wallet';
+ if (isComplete) {
+ progressDescription = `${totalCount} signatures are ready to return to the DApp`;
+ }
+
+ return (
+
+
+
+
+
+
+ {isComplete
+ ? 'All transactions signed'
+ : `Signing transaction ${Math.min(
+ signedCount + 1,
+ totalCount,
+ )} of ${totalCount}`}
+
+
+ {progressDescription}
+
+
+
+
+
+
+
+ {`${signedCount} signed`}
+
+
+ {`${remainingCount} remaining`}
+
+
+
+
+ {!isComplete && currentTransaction ? (
+
+
+ Current transaction
+
+
+
+
+ {`Transaction ${currentTransaction.id}`}
+
+
+ {`To ${currentTransaction.recipient}`}
+
+
+
+ {`−${currentTransaction.amount}`}
+
+
+
+ ) : null}
+
+ {!isComplete ? (
+
+
+
+ Keep your device connected. You will confirm every transaction
+ separately.
+
+
+ ) : null}
+
+ );
+}
+
+function BatchPsbtSigningModalContent() {
+ const route =
+ useRoute<
+ RouteProp<
+ IModalSignatureConfirmParamList,
+ EModalSignatureConfirmRoutes.BatchPsbtConfirm
+ >
+ >();
+ const navigation = useAppNavigation();
+ const { activeAccount } = useActiveAccount({ num: 0 });
+ const walletType = route.params?.walletType ?? 'hardware';
+ const [signedTransactionIds, setSignedTransactionIds] = useState(
+ [],
+ );
+ const [signingStage, setSigningStage] =
+ useState('overview');
+ const [batchTransactionIds, setBatchTransactionIds] = useState([]);
+ const [batchProgressIndex, setBatchProgressIndex] = useState(0);
+
+ const remainingTransactions = useMemo(
+ () =>
+ MOCK_TRANSACTIONS.filter(
+ (transaction) => !signedTransactionIds.includes(transaction.id),
+ ),
+ [signedTransactionIds],
+ );
+ const allSigned = signedTransactionIds.length === MOCK_TRANSACTIONS.length;
+ const currentBatchTransaction = useMemo(() => {
+ const transactionId = batchTransactionIds[batchProgressIndex];
+ return MOCK_TRANSACTIONS.find(
+ (transaction) => transaction.id === transactionId,
+ );
+ }, [batchProgressIndex, batchTransactionIds]);
+
+ useEffect(() => {
+ if (signingStage !== 'signing' || !currentBatchTransaction) {
+ return undefined;
+ }
+
+ const timer = setTimeout(
+ () => {
+ setSignedTransactionIds((currentIds) => [
+ ...new Set([...currentIds, currentBatchTransaction.id]),
+ ]);
+
+ const nextProgressIndex = batchProgressIndex + 1;
+ if (nextProgressIndex >= batchTransactionIds.length) {
+ setSigningStage('complete');
+ } else {
+ setBatchProgressIndex(nextProgressIndex);
+ }
+ },
+ walletType === 'hardware' ? 1800 : 450,
+ );
+
+ return () => clearTimeout(timer);
+ }, [
+ batchProgressIndex,
+ batchTransactionIds.length,
+ currentBatchTransaction,
+ signingStage,
+ walletType,
+ ]);
+
+ const openExistingTransactionConfirm = async (
+ transaction: IMockTransaction,
+ ) => {
+ const account = activeAccount.account;
+ const accountId = account?.id;
+ const networkId = activeAccount.network?.id;
+ const networkCode = activeAccount.network?.code;
+ const accountAddress = account?.address;
+
+ if (
+ !account ||
+ !accountId ||
+ !networkId ||
+ !networkCode ||
+ !accountAddress
+ ) {
+ Toast.error({
+ title: 'Select a Bitcoin account before opening transaction details',
+ });
+ return;
+ }
+
+ let unsignedTx: IUnsignedTxPro;
+ try {
+ const receiveAddressPath = account.addressDetail.receiveAddressPath;
+ const accountRelPath =
+ receiveAddressPath?.split('/').slice(-2).join('/') ||
+ account.relPath ||
+ '0/0';
+ const accountFullPath =
+ receiveAddressPath ||
+ getAccountFullPath({
+ accountPath: account.path,
+ accountRelPath,
+ });
+ const accountXpub = 'xpub' in account ? account.xpub : undefined;
+ let accountPublicKey = account.pub;
+
+ if (accountXpub) {
+ const { getBtcForkNetwork, getPublicKeyFromXpub } =
+ await loadCoreBtcSdk();
+ accountPublicKey = getPublicKeyFromXpub({
+ xpub: accountXpub,
+ network: getBtcForkNetwork(networkCode),
+ relPath: accountRelPath,
+ });
+ }
+ if (!accountPublicKey) {
+ throw new OneKeyLocalError(
+ 'Unable to resolve the active Bitcoin public key',
+ );
+ }
+
+ unsignedTx = buildMockUnsignedTx({
+ transaction,
+ accountId,
+ networkId,
+ accountAddress,
+ accountPublicKey,
+ accountFullPath,
+ });
+ } catch (_error) {
+ Toast.error({
+ title: 'Unable to build the account-matched mock PSBT',
+ });
+ return;
+ }
+
+ navigation.push(EModalSignatureConfirmRoutes.TxConfirm, {
+ accountId,
+ networkId,
+ unsignedTxs: [unsignedTx],
+ signOnly: true,
+ feeInfoEditable: false,
+ popStack: false,
+ onSuccess: () => {
+ setSignedTransactionIds((currentIds) => [
+ ...new Set([...currentIds, transaction.id]),
+ ]);
+ Toast.success({ title: `Transaction ${transaction.id} signed` });
+ },
+ });
+ };
+
+ const startBatchSigning = () => {
+ setBatchTransactionIds(
+ remainingTransactions.map((transaction) => transaction.id),
+ );
+ setBatchProgressIndex(0);
+ setSigningStage('signing');
+ };
+
+ const showBatchSigningNotice = () => {
+ const transactionCount = remainingTransactions.length;
+ Dialog.show({
+ icon: walletType === 'hardware' ? 'LaptopOutline' : 'WalletOutline',
+ title:
+ transactionCount === MOCK_TRANSACTIONS.length
+ ? `Sign all ${transactionCount} transactions?`
+ : `Sign ${transactionCount} remaining transactions?`,
+ description:
+ walletType === 'hardware'
+ ? 'You’ll review and approve each transaction on your hardware wallet. Keep the device connected until signing is complete.'
+ : `Authorize once to sign all ${transactionCount} transactions. Review the transaction summary carefully before continuing, as signing will begin immediately after you confirm.`,
+ renderContent: (
+
+
+ Transactions
+
+ {transactionCount}
+
+ ),
+ onCancelText: 'Cancel',
+ onConfirmText: walletType === 'hardware' ? 'Continue' : 'Sign all',
+ onConfirm: () => {
+ if (walletType === 'hardware') {
+ startBatchSigning();
+ return;
+ }
+ setSignedTransactionIds(MOCK_TRANSACTIONS.map(({ id }) => id));
+ Toast.success({
+ title: `${MOCK_TRANSACTIONS.length} transactions signed`,
+ });
+ },
+ });
+ };
+
+ let confirmText = `Sign all ${MOCK_TRANSACTIONS.length}`;
+ if (allSigned) {
+ confirmText = 'Done';
+ } else if (signedTransactionIds.length) {
+ confirmText = `Sign remaining ${remainingTransactions.length}`;
+ }
+
+ if (signingStage !== 'overview') {
+ return (
+
+
+
+
+
+
+ {
+ if (signingStage === 'complete') {
+ closePageStack();
+ }
+ }}
+ />
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Transactions
+
+
+ {`${remainingTransactions.length} remaining`}
+
+
+
+ {MOCK_TRANSACTIONS.map((transaction) => (
+ void openExistingTransactionConfirm(transaction)}
+ />
+ ))}
+
+
+
+
+
+ {
+ if (allSigned) {
+ closePageStack();
+ return;
+ }
+ showBatchSigningNotice();
+ }}
+ onCancelText={allSigned ? undefined : 'Reject all'}
+ cancelButtonProps={allSigned ? undefined : { variant: 'secondary' }}
+ />
+
+
+ );
+}
+
+export const BatchPsbtSigningModal = () => (
+
+
+
+);
+
+function WalletFlowOption({
+ icon,
+ title,
+ description,
+ onPress,
+}: {
+ icon: 'WalletOutline' | 'LaptopOutline';
+ title: string;
+ description: string;
+ onPress: () => void;
+}) {
+ return (
+
+
+
+
+
+ {title}
+
+ {description}
+
+
+
+
+ );
+}
+
+function BatchPsbtSigningGallery() {
+ const navigation = useAppNavigation();
+ const openModal = (walletType: IBatchPsbtWalletType) => {
+ navigation.pushModal(EModalRoutes.SignatureConfirmModal, {
+ screen: EModalSignatureConfirmRoutes.BatchPsbtConfirm,
+ params: { walletType },
+ });
+ };
+ const openSoftwareWallet = () => openModal('software');
+ const openHardwareWallet = () => openModal('hardware');
+
+ return (
+
+
+
+
+
+ Choose a wallet type
+
+ Preview each batch signing flow separately.
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default BatchPsbtSigningGallery;
diff --git a/packages/kit/src/views/Developer/pages/Gallery/index.tsx b/packages/kit/src/views/Developer/pages/Gallery/index.tsx
index f083a5aa6522..1faacef79559 100644
--- a/packages/kit/src/views/Developer/pages/Gallery/index.tsx
+++ b/packages/kit/src/views/Developer/pages/Gallery/index.tsx
@@ -511,6 +511,11 @@ const CryptoGallery = LazyLoadPage(
import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/CryptoGallery'),
);
+const BatchPsbtSigningGallery = LazyLoadPage(
+ () =>
+ import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/BatchPsbtSigning'),
+);
+
const UnOrderedListGallery = LazyLoadPage(
() => import('./Components/stories/UnOrderedListGallery'),
);
@@ -860,6 +865,10 @@ export const galleryScreenList: {
name: EGalleryRoutes.ComponentCryptoGallery,
component: CryptoGallery,
},
+ {
+ name: EGalleryRoutes.ComponentBatchPsbtSigning,
+ component: BatchPsbtSigningGallery,
+ },
{
name: EGalleryRoutes.ComponentUnOrderedList,
component: UnOrderedListGallery,
diff --git a/packages/kit/src/views/Setting/pages/Tab/DevSettingsSection/index.tsx b/packages/kit/src/views/Setting/pages/Tab/DevSettingsSection/index.tsx
index 56cf97b16b30..ec247b8436e6 100644
--- a/packages/kit/src/views/Setting/pages/Tab/DevSettingsSection/index.tsx
+++ b/packages/kit/src/views/Setting/pages/Tab/DevSettingsSection/index.tsx
@@ -765,7 +765,7 @@ const BaseDevSettingsSection = () => {
title: 'UI Galleries',
description: 'UI Galleries',
keywords:
- 'DesktopApiProxy Test PerpGallery CryptoGallery CloudBackupGallery AuthGallery KeylessWalletGallery StorageGallery',
+ 'DesktopApiProxy Test Batch PSBT Signing Bitcoin PerpGallery CryptoGallery CloudBackupGallery AuthGallery KeylessWalletGallery StorageGallery',
},
{
key: 'account',
@@ -2073,6 +2073,17 @@ const BaseDevSettingsSection = () => {
}}
/>
+ {
+ navigation.push(
+ EModalSettingRoutes.SettingDevBatchPsbtSigningModal,
+ );
+ }}
+ />
+
+ import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/BatchPsbtSigning'),
+);
+
const PerpGallery = LazyLoadPage(
() =>
import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/PerpGallery'),
@@ -258,6 +263,10 @@ export const BasicModalSettingStack: IModalFlowNavigatorConfig<
name: EModalSettingRoutes.SettingDevDesktopApiProxyTestModal,
component: DesktopApiProxyTestDevSettings,
},
+ {
+ name: EModalSettingRoutes.SettingDevBatchPsbtSigningModal,
+ component: BatchPsbtSigningGallery,
+ },
{
name: EModalSettingRoutes.SettingDevPerpGalleryModal,
component: PerpGallery,
diff --git a/packages/kit/src/views/SignatureConfirm/router/index.tsx b/packages/kit/src/views/SignatureConfirm/router/index.tsx
index d6a287ca9a6b..904403242b7d 100644
--- a/packages/kit/src/views/SignatureConfirm/router/index.tsx
+++ b/packages/kit/src/views/SignatureConfirm/router/index.tsx
@@ -56,6 +56,12 @@ const TxConfirm = LazyLoadPage(
import('@onekeyhq/kit/src/views/SignatureConfirm/pages/TxConfirm/TxConfirm'),
);
+const BatchPsbtConfirm = LazyLoadPage(() =>
+ import('@onekeyhq/kit/src/views/Developer/pages/Gallery/Components/stories/BatchPsbtSigning').then(
+ ({ BatchPsbtSigningModal }) => ({ default: BatchPsbtSigningModal }),
+ ),
+);
+
const MessageConfirm = LazyLoadPage(
() =>
import('@onekeyhq/kit/src/views/SignatureConfirm/pages/MessageConfirm/MessageConfirm'),
@@ -85,6 +91,10 @@ export const ModalSignatureConfirmStack: IModalFlowNavigatorConfig<
EModalSignatureConfirmRoutes,
IModalSignatureConfirmParamList
>[] = [
+ {
+ name: EModalSignatureConfirmRoutes.BatchPsbtConfirm,
+ component: BatchPsbtConfirm,
+ },
{
name: EModalSignatureConfirmRoutes.TxConfirm,
component: TxConfirm,
diff --git a/packages/shared/src/routes/gallery.ts b/packages/shared/src/routes/gallery.ts
index 5f2e1dbc2500..66e75b46e203 100644
--- a/packages/shared/src/routes/gallery.ts
+++ b/packages/shared/src/routes/gallery.ts
@@ -17,6 +17,7 @@ export enum EGalleryRoutes {
ComponentColorPicker = 'component-ColorPicker',
ComponentCurrency = 'component-Currency',
ComponentCryptoGallery = 'component-CryptoGallery',
+ ComponentBatchPsbtSigning = 'component-BatchPsbtSigning',
ComponentCarousel = 'component-Carousel',
ComponentDatePicker = 'component-DatePicker',
ComponentDialog = 'component-Dialog',
diff --git a/packages/shared/src/routes/setting.ts b/packages/shared/src/routes/setting.ts
index c96a6530282f..a242ebc4d570 100644
--- a/packages/shared/src/routes/setting.ts
+++ b/packages/shared/src/routes/setting.ts
@@ -26,6 +26,7 @@ export enum EModalSettingRoutes {
SettingDevUnitTestsModal = 'SettingDevUnitTestsModal',
SettingDevSesHardenRuntimeCheckModal = 'SettingDevSesHardenRuntimeCheckModal',
SettingDevDesktopApiProxyTestModal = 'SettingDevDesktopApiProxyTestModal',
+ SettingDevBatchPsbtSigningModal = 'SettingDevBatchPsbtSigningModal',
SettingDevPerpGalleryModal = 'SettingDevPerpGalleryModal',
SettingDevCryptoGalleryModal = 'SettingDevCryptoGalleryModal',
SettingDevCloudBackupGalleryModal = 'SettingDevCloudBackupGalleryModal',
@@ -102,6 +103,7 @@ export type IModalSettingParamList = {
[EModalSettingRoutes.SettingDevUnitTestsModal]: undefined;
[EModalSettingRoutes.SettingDevSesHardenRuntimeCheckModal]: undefined;
[EModalSettingRoutes.SettingDevDesktopApiProxyTestModal]: undefined;
+ [EModalSettingRoutes.SettingDevBatchPsbtSigningModal]: undefined;
[EModalSettingRoutes.SettingDevPerpGalleryModal]: undefined;
[EModalSettingRoutes.SettingDevCryptoGalleryModal]: undefined;
[EModalSettingRoutes.SettingDevCloudBackupGalleryModal]: undefined;
diff --git a/packages/shared/src/routes/signatureConfirm.ts b/packages/shared/src/routes/signatureConfirm.ts
index c07980275f60..d9bf19952de8 100644
--- a/packages/shared/src/routes/signatureConfirm.ts
+++ b/packages/shared/src/routes/signatureConfirm.ts
@@ -31,6 +31,7 @@ import type { IToken, ITokenFiat } from '../../types/token';
import type { EReplaceTxType, ISendTxOnSuccessData } from '../../types/tx';
export enum EModalSignatureConfirmRoutes {
+ BatchPsbtConfirm = 'BatchPsbtConfirm',
TxDataInput = 'TxDataInput',
TxAmountInput = 'TxAmountInput',
TxConfirm = 'TxConfirm',
@@ -51,7 +52,12 @@ export enum EModalSignatureConfirmRoutes {
WeblnSendPayment = 'WeblnSendPayment',
}
+export type IBatchPsbtWalletType = 'software' | 'hardware';
+
export type IModalSignatureConfirmParamList = {
+ [EModalSignatureConfirmRoutes.BatchPsbtConfirm]: {
+ walletType: IBatchPsbtWalletType;
+ };
[EModalSignatureConfirmRoutes.TxSelectToken]: ITokenSelectorParamList;
[EModalSignatureConfirmRoutes.TxSelectAggregateToken]: IAggregateTokenSelectorParams;
[EModalSignatureConfirmRoutes.TxDataInput]: {