Skip to content

Commit

Permalink
feat(rpc): stx_getAddresses, closes LEA-1958
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Feb 5, 2025
1 parent 9972d22 commit e463bcd
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 97 deletions.
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 @@ -13,6 +13,7 @@ import { rpcSendTransfer } from './rpc-methods/send-transfer';
import { rpcSignMessage } from './rpc-methods/sign-message';
import { rpcSignPsbt } from './rpc-methods/sign-psbt';
import { rpcSignStacksMessage } from './rpc-methods/sign-stacks-message';
import { rpcStxGetAddresses } from './rpc-methods/stx-get-addresses';
import { rpcSupportedMethods } from './rpc-methods/supported-methods';

export async function rpcMessageHandler(message: WalletRequests, port: chrome.runtime.Port) {
Expand Down Expand Up @@ -62,6 +63,11 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru
break;
}

case 'stx_getAddresses': {
await rpcStxGetAddresses(message, port);
break;
}

default:
chrome.tabs.sendMessage(
getTabIdFromPort(port),
Expand Down
38 changes: 22 additions & 16 deletions src/background/messaging/rpc-methods/get-addresses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type GetAddressesRequest, RpcErrorCode } from '@leather.io/rpc';
import { type LeatherRpcMethodMap, type MethodNames, RpcErrorCode } from '@leather.io/rpc';

import { RouteUrls } from '@shared/route-urls';
import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods';
Expand All @@ -10,20 +10,26 @@ import {
} from '../messaging-utils';
import { trackRpcRequestSuccess } from '../rpc-message-handler';

export async function rpcGetAddresses(message: GetAddressesRequest, port: chrome.runtime.Port) {
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [['requestId', message.id]]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcGetAddresses, urlParams);
void trackRpcRequestSuccess({ endpoint: message.method });
export function makeRpcAddressesMessageListerner<T extends MethodNames>(
eventName: 'getAddresses' | 'stx_getAddresses'
) {
return async (message: LeatherRpcMethodMap[T]['request'], port: chrome.runtime.Port) => {
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [['requestId', message.id]]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RpcGetAddresses, urlParams);
void trackRpcRequestSuccess({ endpoint: message.method });

listenForPopupClose({
tabId,
id,
response: makeRpcErrorResponse('getAddresses', {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected request to get addresses',
},
}),
});
listenForPopupClose({
tabId,
id,
response: makeRpcErrorResponse(eventName, {
id: message.id,
error: {
code: RpcErrorCode.USER_REJECTION,
message: 'User rejected request to get addresses',
},
}),
});
};
}

export const rpcGetAddresses = makeRpcAddressesMessageListerner('getAddresses');
4 changes: 4 additions & 0 deletions src/background/messaging/rpc-methods/stx-get-addresses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { makeRpcAddressesMessageListerner } from './get-addresses';

// Method simply clones behaviour of getAddresses action
export const rpcStxGetAddresses = makeRpcAddressesMessageListerner('stx_getAddresses');
171 changes: 90 additions & 81 deletions tests/specs/rpc-get-addresses/get-addresses.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import type { SupportedBlockchains } from '@leather.io/models';

import { test } from '../../fixtures/fixtures';

const getAddressesMethods = ['getAddresses', 'stx_getAddresses'] as const;
type GetAddressesMethods = (typeof getAddressesMethods)[number];

function softwareBeforeEach() {
return () =>
test.beforeEach(
Expand Down Expand Up @@ -62,8 +65,11 @@ async function interceptRequestPopup(context: BrowserContext) {
return context.waitForEvent('page');
}

async function initiateGetAddresses(page: Page) {
return page.evaluate(async () => (window as any).LeatherProvider?.request('getAddresses'));
async function initiateGetAddresses(page: Page, eventName: GetAddressesMethods) {
return page.evaluate(
async eventName => (window as any).LeatherProvider?.request(eventName),
eventName
);
}

async function clickConnectLeatherButton(popup: Page) {
Expand All @@ -72,93 +78,96 @@ async function clickConnectLeatherButton(popup: Page) {
await button.click();
}

test.describe('Rpc: GetAddresses', () => {
test.beforeEach(
async ({ extensionId, globalPage }) => await globalPage.setupAndUseApiCalls(extensionId)
);

const specs = {
softwareWallet: {
beforeEach: softwareBeforeEach(),
expectedResult: getExpectedResponseForKeys(['bitcoin', 'stacks']),
},
ledgerWithBitcoinAndStacksKey: {
beforeEach: ledgerBeforeEach(makeLedgerTestAccountWalletState(['bitcoin', 'stacks'])),
expectedResult: getExpectedResponseForKeys(['bitcoin', 'stacks']),
},
ledgerWithStacksKeysOnly: {
beforeEach: ledgerBeforeEach(makeLedgerTestAccountWalletState(['stacks'])),
expectedResult: getExpectedResponseForKeys(['stacks']),
},
ledgerWithBitcoinKeysOnly: {
beforeEach: ledgerBeforeEach(makeLedgerTestAccountWalletState(['bitcoin'])),
expectedResult: getExpectedResponseForKeys(['bitcoin']),
},
} as const;

for (const [walletPreset, { beforeEach, expectedResult }] of Object.entries(specs)) {
test.describe(`${walletPreset} `, () => {
beforeEach();

test('the promise resolves with addresses successfully', async ({ page, context }) => {
await page.goto('localhost:3000');
const getAddressesPromise = initiateGetAddresses(page);

const popup = await interceptRequestPopup(context);
await clickConnectLeatherButton(popup);

const result = await getAddressesPromise;
if (!result) throw new Error('Expected result');
const { id, ...payloadWithoutId } = result;

test.expect(payloadWithoutId).toEqual(expectedResult);
});

test('the promise rejects when user closes popup window', async ({ page, context }) => {
await page.goto('localhost:3000');
const getAddressesPromise = initiateGetAddresses(page);
const popup = await interceptRequestPopup(context);
await popup.close();
await test.expect(getAddressesPromise).rejects.toThrow();
});
//
getAddressesMethods.forEach(method => {
test.describe(`Rpc: ${method}`, () => {
test.beforeEach(
async ({ extensionId, globalPage }) => await globalPage.setupAndUseApiCalls(extensionId)
);

if (walletPreset === 'softwareWallet') {
test('it redirects back to get addresses flow when wallet is locked', async ({
homePage,
page,
context,
}) => {
await homePage.lock();
const specs = {
softwareWallet: {
beforeEach: softwareBeforeEach(),
expectedResult: getExpectedResponseForKeys(['bitcoin', 'stacks']),
},
ledgerWithBitcoinAndStacksKey: {
beforeEach: ledgerBeforeEach(makeLedgerTestAccountWalletState(['bitcoin', 'stacks'])),
expectedResult: getExpectedResponseForKeys(['bitcoin', 'stacks']),
},
ledgerWithStacksKeysOnly: {
beforeEach: ledgerBeforeEach(makeLedgerTestAccountWalletState(['stacks'])),
expectedResult: getExpectedResponseForKeys(['stacks']),
},
ledgerWithBitcoinKeysOnly: {
beforeEach: ledgerBeforeEach(makeLedgerTestAccountWalletState(['bitcoin'])),
expectedResult: getExpectedResponseForKeys(['bitcoin']),
},
} as const;

Object.entries(specs).forEach(([walletPreset, { beforeEach, expectedResult }]) => {
test.describe(`${walletPreset} `, () => {
beforeEach();

test('the promise resolves with addresses successfully', async ({ page, context }) => {
await page.goto('localhost:3000');
const getAddressesPromise = initiateGetAddresses(page);
const getAddressesPromise = initiateGetAddresses(page, method);

const popup = await interceptRequestPopup(context);
await popup.getByRole('textbox').fill(TEST_PASSWORD);
await popup.getByRole('button', { name: 'Continue' }).click();
await popup.getByText('Connect').isVisible();
await test.expect(popup.getByTestId('get-addresses-approve-button')).toBeVisible();
await clickConnectLeatherButton(popup);
await test.expect(getAddressesPromise).resolves.toMatchObject(expectedResult);

const result = await getAddressesPromise;
if (!result) throw new Error('Expected result');
const { id, ...payloadWithoutId } = result;

test.expect(payloadWithoutId).toEqual(expectedResult);
});

test('it returns the second accounts data after changing account', async ({
page,
context,
}) => {
test('the promise rejects when user closes popup window', async ({ page, context }) => {
await page.goto('localhost:3000');
const getAddressesPromise = initiateGetAddresses(page);
const getAddressesPromise = initiateGetAddresses(page, method);
const popup = await interceptRequestPopup(context);
const switchAccountButton = popup.getByTestId('switch-account-item-0');
await switchAccountButton.click();
const secondAccountInListButton = popup.getByTestId('switch-account-item-1');
await secondAccountInListButton.click();
await test.expect(popup.getByText('Account 2')).toBeVisible();
await clickConnectLeatherButton(popup);
const result = await getAddressesPromise;
test
.expect(result.result.addresses[0].address)
.toEqual('bc1qr9wmz342txkcmxxq5ysmfqrd5rvmxtu6nldjgp');
await popup.close();
await test.expect(getAddressesPromise).rejects.toThrow();
});
}

if (walletPreset === 'softwareWallet') {
test('it redirects back to get addresses flow when wallet is locked', async ({
homePage,
page,
context,
}) => {
await homePage.lock();
await page.goto('localhost:3000');
const getAddressesPromise = initiateGetAddresses(page, method);
const popup = await interceptRequestPopup(context);
await popup.getByRole('textbox').fill(TEST_PASSWORD);
await popup.getByRole('button', { name: 'Continue' }).click();
await popup.getByText('Connect').isVisible();
await test.expect(popup.getByTestId('get-addresses-approve-button')).toBeVisible();
await clickConnectLeatherButton(popup);
await test.expect(getAddressesPromise).resolves.toMatchObject(expectedResult);
});

test('it returns the second accounts data after changing account', async ({
page,
context,
}) => {
await page.goto('localhost:3000');
const getAddressesPromise = initiateGetAddresses(page, method);
const popup = await interceptRequestPopup(context);
const switchAccountButton = popup.getByTestId('switch-account-item-0');
await switchAccountButton.click();
const secondAccountInListButton = popup.getByTestId('switch-account-item-1');
await secondAccountInListButton.click();
await test.expect(popup.getByText('Account 2')).toBeVisible();
await clickConnectLeatherButton(popup);
const result = await getAddressesPromise;
test
.expect(result.result.addresses[0].address)
.toEqual('bc1qr9wmz342txkcmxxq5ysmfqrd5rvmxtu6nldjgp');
});
}
});
});
}
});
});

0 comments on commit e463bcd

Please sign in to comment.