Skip to content

Commit

Permalink
feat: add rpc-get-xpub method
Browse files Browse the repository at this point in the history
  • Loading branch information
vinlim authored and kyranjamie committed Apr 10, 2024
1 parent b4b1d11 commit 2862ba1
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 1 deletion.
54 changes: 54 additions & 0 deletions src/app/pages/rpc-get-xpub/components/get-xpub.layout.tsx
Original file line number Diff line number Diff line change
@@ -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 { LeatherIcon } from '@app/ui/components/icons/leather-icon';

Check failure on line 5 in src/app/pages/rpc-get-xpub/components/get-xpub.layout.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find module '@app/ui/components/icons/leather-icon' or its corresponding type declarations.
import { LeatherLIcon } from '@app/ui/components/icons/leather-l-icon';

Check failure on line 6 in src/app/pages/rpc-get-xpub/components/get-xpub.layout.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Cannot find module '@app/ui/components/icons/leather-l-icon' or its corresponding type declarations.

interface GetXpubLayoutProps {
requester: string;
onUserApproveGetXpub(): void;
}
export function GetXpubLayout(props: GetXpubLayoutProps) {
const { requester, onUserApproveGetXpub } = props;

return (
<Flex flexDirection="column" height="100vh" width="100%">
<Flex
flex={1}
flexDirection="column"
textAlign="center"
alignItems="center"
p="space.06"
gap="space.06"
>
<Box mb="space.08" mt="space.11">
<LeatherIcon width="248px" height="58px" />
</Box>
<styled.p textStyle="heading.03">Connect your account to</styled.p>

<RequesterFlag requester={requester} />
<Box width="100%" display="flex">
<Button onClick={() => onUserApproveGetXpub()} fullWidth>
<Flex justifyContent="center" alignItems="center">
<LeatherLIcon mr="space.02" />
<styled.span textStyle="label.02">Connect Leather</styled.span>
</Flex>
</Button>
</Box>
</Flex>
<Flex
px="space.05"
py="space.03"
lineHeight="20px"
textAlign="center"
alignSelf="bottom"
bg="accent.background-secondary"
>
<styled.p textStyle="caption.02">
By connecting you give permission to {requester} to view Xpub of this account
</styled.p>
</Flex>
</Flex>
);
}
17 changes: 17 additions & 0 deletions src/app/pages/rpc-get-xpub/rpc-get-xpub.tsx
Original file line number Diff line number Diff line change
@@ -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 <GetXpubLayout requester={requester} onUserApproveGetXpub={onUserApproveGetXpub} />;
}
58 changes: 58 additions & 0 deletions src/app/pages/rpc-get-xpub/use-request-accounts.ts
Original file line number Diff line number Diff line change
@@ -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();
},
};
}
9 changes: 9 additions & 0 deletions src/app/routes/rpc-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,6 +25,14 @@ export const rpcRequestRoutes = (
</AccountGate>
}
/>
<Route
path={RouteUrls.RpcGetXpub}
element={
<AccountGate>
<RpcGetXpub />
</AccountGate>
}
/>
{rpcSendTransferRoutes}
<Route
path={RouteUrls.RpcSignBip322Message}
Expand Down
6 changes: 6 additions & 0 deletions src/background/messaging/rpc-message-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RpcErrorCode } from '@btckit/types';

import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods';

import { rpcGetXpub } from '@background/messaging/rpc-methods/get-xpub';
import { rpcSignStacksTransaction } from '@background/messaging/rpc-methods/sign-stacks-transaction';

import { getTabIdFromPort } from './messaging-utils';
Expand All @@ -20,6 +21,11 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
break;
}

case 'getXpub': {
await rpcGetXpub(message, port);
break;
}

case 'signMessage': {
await rpcSignMessage(message, port);
break;
Expand Down
30 changes: 30 additions & 0 deletions src/background/messaging/rpc-methods/get-xpub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { RpcErrorCode } from '@btckit/types';

import { RouteUrls } from '@shared/route-urls';
import { GetXpubRequest } from '@shared/rpc/methods/get-xpub';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';

import {
listenForPopupClose,
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '../messaging-utils';

export async function rpcGetXpub(message: GetXpubRequest, port: chrome.runtime.Port) {
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [['requestId', message.id]]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcGetXpub, urlParams);
listenForPopupClose({
tabId,
id,
response: {
id: message.id,
result: makeRpcErrorResponse('getXpub', {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected request to get xpub',
},
}),
},
});
}
1 change: 1 addition & 0 deletions src/shared/route-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export enum RouteUrls {

// Request routes bitcoin
RpcGetAddresses = '/get-addresses',
RpcGetXpub = '/get-xpub',
RpcSignPsbt = '/sign-psbt',
RpcSignPsbtSummary = '/sign-psbt/summary',
RpcSendTransfer = '/send-transfer',
Expand Down
7 changes: 7 additions & 0 deletions src/shared/rpc/methods/get-xpub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { DefineRpcMethod, RpcRequest, RpcResponse } from '@btckit/types';

export type GetXpubRequest = RpcRequest<'getXpub'>;

type GetXpubResponse = RpcResponse<{ xpubs: [] }>;

export type GetXpub = DefineRpcMethod<GetXpubRequest, GetXpubResponse>;
4 changes: 3 additions & 1 deletion src/shared/rpc/rpc-methods.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -14,7 +15,8 @@ export type WalletMethodMap = BtcKitMethodMap &
SignPsbt &
AcceptBitcoinContract &
SignStacksTransaction &
SignStacksMessage;
SignStacksMessage &
GetXpub;

export type WalletRequests = ValueOf<WalletMethodMap>['request'];
export type WalletResponses = ValueOf<WalletMethodMap>['response'];
Expand Down
4 changes: 4 additions & 0 deletions tests/mocks/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
43 changes: 43 additions & 0 deletions tests/specs/rpc-get-xpub/get-xpub.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 2862ba1

Please sign in to comment.