diff --git a/src/app/pages/rpc-get-xpub/components/get-xpub.layout.tsx b/src/app/pages/rpc-get-xpub/components/get-xpub.layout.tsx
new file mode 100644
index 00000000000..b2329960aa7
--- /dev/null
+++ b/src/app/pages/rpc-get-xpub/components/get-xpub.layout.tsx
@@ -0,0 +1,54 @@
+import { Box, Flex, styled } from 'leather-styles/jsx';
+
+import { RequesterFlag } from '@app/components/requester-flag';
+import { Button } from '@app/ui/components/button/button';
+import { LettermarkIcon } from '@app/ui/icons';
+import { LogomarkIcon } from '@app/ui/icons/logomark-icon';
+
+interface GetXpubLayoutProps {
+ requester: string;
+ onUserApproveGetXpub(): void;
+}
+export function GetXpubLayout(props: GetXpubLayoutProps) {
+ const { requester, onUserApproveGetXpub } = props;
+
+ return (
+
+
+
+
+
+ Connect your account to
+
+
+
+
+
+
+
+
+ By connecting you give permission to {requester} to view Xpub of this account
+
+
+
+ );
+}
diff --git a/src/app/pages/rpc-get-xpub/rpc-get-xpub.tsx b/src/app/pages/rpc-get-xpub/rpc-get-xpub.tsx
new file mode 100644
index 00000000000..6dc910fcabb
--- /dev/null
+++ b/src/app/pages/rpc-get-xpub/rpc-get-xpub.tsx
@@ -0,0 +1,17 @@
+import { closeWindow } from '@shared/utils';
+
+import { GetXpubLayout } from './components/get-xpub.layout';
+import { useGetXpub } from './use-request-accounts';
+
+export function RpcGetXpub() {
+ const { origin, onUserApproveGetXpub } = useGetXpub();
+
+ if (origin === null) {
+ closeWindow();
+ throw new Error('Origin is null');
+ }
+
+ const requester = new URL(origin).host;
+
+ return ;
+}
diff --git a/src/app/pages/rpc-get-xpub/use-request-accounts.ts b/src/app/pages/rpc-get-xpub/use-request-accounts.ts
new file mode 100644
index 00000000000..dcd4f083fc4
--- /dev/null
+++ b/src/app/pages/rpc-get-xpub/use-request-accounts.ts
@@ -0,0 +1,58 @@
+import { deriveNativeSegwitAccountFromRootKeychain } from '@shared/crypto/bitcoin/p2wpkh-address-gen';
+import { logger } from '@shared/logger';
+import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
+import { closeWindow } from '@shared/utils';
+
+import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
+import { mnemonicToRootNode } from '@app/common/keychain/keychain';
+import { useRpcRequestParams } from '@app/common/rpc-helpers';
+import { useCurrentAccountIndex } from '@app/store/accounts/account';
+import { useAppPermissions } from '@app/store/app-permissions/app-permissions.slice';
+import { useDefaultWalletSecretKey } from '@app/store/in-memory-key/in-memory-key.selectors';
+
+export function useGetXpub() {
+ const analytics = useAnalytics();
+
+ const permissions = useAppPermissions();
+ const { tabId, origin, requestId } = useRpcRequestParams();
+
+ const currentAccountIndex = useCurrentAccountIndex();
+ const secretKey = useDefaultWalletSecretKey();
+ const rootKey = secretKey ? mnemonicToRootNode(secretKey) : null;
+
+ return {
+ origin,
+ onUserApproveGetXpub() {
+ if (!tabId || !origin) {
+ logger.error('Cannot give app accounts: missing tabId, origin');
+ return;
+ }
+
+ const keysToIncludeInResponse = [];
+
+ if (rootKey) {
+ const createBitcoinAccount = deriveNativeSegwitAccountFromRootKeychain(rootKey, 'mainnet');
+ const currentBitcoinAccount = createBitcoinAccount(currentAccountIndex);
+
+ const nativeSegwitXpubResponse: any = {
+ symbol: 'BTC',
+ type: 'p2wpkh',
+ xpub: currentBitcoinAccount.keychain.publicExtendedKey,
+ };
+
+ keysToIncludeInResponse.push(nativeSegwitXpubResponse);
+ }
+
+ void analytics.track('user_approved_get_xpub', { origin });
+ permissions.hasRequestedAccounts(origin);
+ chrome.tabs.sendMessage(
+ tabId,
+ makeRpcSuccessResponse('getXpub', {
+ id: requestId,
+ result: { xpubs: keysToIncludeInResponse as any },
+ })
+ );
+ closeWindow();
+ },
+ };
+}
diff --git a/src/app/routes/rpc-routes.tsx b/src/app/routes/rpc-routes.tsx
index 8893db05c47..eb722e8bd2d 100644
--- a/src/app/routes/rpc-routes.tsx
+++ b/src/app/routes/rpc-routes.tsx
@@ -6,6 +6,7 @@ import { RouteUrls } from '@shared/route-urls';
import { ledgerBitcoinTxSigningRoutes } from '@app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container';
import { ledgerStacksMessageSigningRoutes } from '@app/features/ledger/flows/stacks-message-signing/ledger-stacks-sign-msg.routes';
import { RpcGetAddresses } from '@app/pages/rpc-get-addresses/rpc-get-addresses';
+import { RpcGetXpub } from '@app/pages/rpc-get-xpub/rpc-get-xpub';
import { rpcSendTransferRoutes } from '@app/pages/rpc-send-transfer/rpc-send-transfer.routes';
import { RpcSignPsbt } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt';
import { RpcSignPsbtSummary } from '@app/pages/rpc-sign-psbt/rpc-sign-psbt-summary';
@@ -24,6 +25,14 @@ export const rpcRequestRoutes = (
}
/>
+
+
+
+ }
+ />
{rpcSendTransferRoutes}
;
+
+type GetXpubResponse = RpcResponse<{ xpubs: [] }>;
+
+export type GetXpub = DefineRpcMethod;
diff --git a/src/shared/rpc/rpc-methods.ts b/src/shared/rpc/rpc-methods.ts
index 74460b67f7a..282177d46d0 100644
--- a/src/shared/rpc/rpc-methods.ts
+++ b/src/shared/rpc/rpc-methods.ts
@@ -1,5 +1,6 @@
import { BtcKitMethodMap, ExtractErrorResponse, ExtractSuccessResponse } from '@btckit/types';
+import { GetXpub } from '@shared/rpc/methods/get-xpub';
import { SignStacksTransaction } from '@shared/rpc/methods/sign-stacks-transaction';
import { ValueOf } from '@shared/utils/type-utils';
@@ -14,7 +15,8 @@ export type WalletMethodMap = BtcKitMethodMap &
SignPsbt &
AcceptBitcoinContract &
SignStacksTransaction &
- SignStacksMessage;
+ SignStacksMessage &
+ GetXpub;
export type WalletRequests = ValueOf['request'];
export type WalletResponses = ValueOf['response'];
diff --git a/tests/mocks/constants.ts b/tests/mocks/constants.ts
index af9b99da5a2..8075a29b554 100644
--- a/tests/mocks/constants.ts
+++ b/tests/mocks/constants.ts
@@ -11,6 +11,10 @@ export const TEST_ACCOUNT_1_STX_ADDRESS = 'SPS8CKF63P16J28AYF7PXW9E5AACH0NZNTEFW
export const TEST_ACCOUNT_2_STX_ADDRESS = 'SPXH3HNBPM5YP15VH16ZXZ9AX6CK289K3MCXRKCB';
export const TEST_TESTNET_ACCOUNT_2_STX_ADDRESS = 'STXH3HNBPM5YP15VH16ZXZ9AX6CK289K3NVR9T1P';
+// Account extended public keys
+export const TEST_ACCOUNT_1_XPUB =
+ 'xpub6BuKrNqTrGfsy8VAAdUW2KCxbHywuSKjg7hZuAXERXDv7GfuxUgUWdVRKNsgujcwdjEHCjaXWouPKi1m5gMgdWX8JpRcyMkrSxPe4Da3Lx8';
+
// Account public keys
export const TEST_ACCOUNT_1_PUBKEY =
'02b6b0afe5f620bc8e532b640b148dd9dea0ed19d11f8ab420fcce488fe3974893';
diff --git a/tests/specs/rpc-get-xpub/get-xpub.spec.ts b/tests/specs/rpc-get-xpub/get-xpub.spec.ts
new file mode 100644
index 00000000000..1ee28b4141d
--- /dev/null
+++ b/tests/specs/rpc-get-xpub/get-xpub.spec.ts
@@ -0,0 +1,43 @@
+import { BrowserContext, Page } from '@playwright/test';
+import { TEST_ACCOUNT_1_XPUB } from '@tests/mocks/constants';
+
+import { test } from '../../fixtures/fixtures';
+
+test.describe('RPC get Xpub', () => {
+ test.beforeEach(async ({ extensionId, globalPage, onboardingPage, page }) => {
+ await globalPage.setupAndUseApiCalls(extensionId);
+ await onboardingPage.signInWithTestAccount(extensionId);
+ await page.goto('localhost:3000', { waitUntil: 'networkidle' });
+ });
+
+ function checkVisibleContent(context: BrowserContext) {
+ return async (buttonToPress: 'Cancel' | 'Confirm') => {
+ const popup = await context.waitForEvent('page');
+ await popup.waitForTimeout(500);
+ const btn = popup.locator('text="Connect Leather"');
+
+ if (buttonToPress === 'Confirm') {
+ await btn.click();
+ } else {
+ await popup.close();
+ }
+ };
+ }
+
+ function initiateRPCGetXpub(page: Page) {
+ return async () =>
+ page.evaluate(async () =>
+ (window as any).LeatherProvider.request('getXpub').catch((e: unknown) => e)
+ );
+ }
+
+ test('that xpub is correct', async ({ page, context }) => {
+ const xpub = TEST_ACCOUNT_1_XPUB;
+ const [result] = await Promise.all([
+ initiateRPCGetXpub(page)(),
+ checkVisibleContent(context)('Confirm'),
+ ]);
+
+ test.expect(result.result.xpubs[0].xpub).toEqual(xpub);
+ });
+});