feat: Get network [ENG-6213] (#866)

This commit is contained in:
Edu
2025-01-27 13:39:26 +01:00
committed by GitHub
parent 3ca567afee
commit d19617b977
13 changed files with 145 additions and 56 deletions

17
package-lock.json generated
View File

@@ -15,10 +15,10 @@
"@phosphor-icons/react": "2.1.7",
"@playwright/test": "1.46.1",
"@react-spring/web": "9.7.3",
"@sats-connect/core": "0.5.2",
"@sats-connect/core": "0.5.3-b107fb6",
"@scure/base": "1.1.9",
"@scure/btc-signer": "1.2.1",
"@secretkeylabs/xverse-core": "38.0.0",
"@secretkeylabs/xverse-core": "38.1.1",
"@stacks/connect": "7.9.0",
"@stacks/stacks-blockchain-api-types": "7.14.1",
"@stacks/transactions": "7.0.2",
@@ -1474,9 +1474,10 @@
}
},
"node_modules/@sats-connect/core": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.5.2.tgz",
"integrity": "sha512-i9bcmGX+ljJ/5MbCiu8CcpwOLNTgwV4MzOcItl0wzMswO7TMJ2wjTxjhjf3A6029Wbb+AKun5OLkmDf0jhuvCA==",
"version": "0.5.3-b107fb6",
"resolved": "https://registry.npmjs.org/@sats-connect/core/-/core-0.5.3-b107fb6.tgz",
"integrity": "sha512-SrEYcCh4P6vhGUtRbsqrrs59qEPW9W9ESbk7DUyDATwhbzTLdp2L/Dg2goNKsU1K0/PidqOFu4DOigRC6x2hHg==",
"license": "ISC",
"dependencies": {
"axios": "1.7.7",
"bitcoin-address-validation": "2.2.3",
@@ -1593,9 +1594,9 @@
}
},
"node_modules/@secretkeylabs/xverse-core": {
"version": "38.0.0",
"resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/38.0.0/64b997629f051643c9350450f0b23fc7e44db902",
"integrity": "sha512-YoBgVS0a8tDKzN2xPM0A8qkiyJfk/uRQsGi2pSdrD5uMaKkuZiH+bAK/mEYa1sqE7tqcLlBOWkuCc/dWC+Pl+Q==",
"version": "38.1.1",
"resolved": "https://npm.pkg.github.com/download/@secretkeylabs/xverse-core/38.1.1/714acf65ecd7c61826013538d0e7f2825cb8250e",
"integrity": "sha512-YhiQ4TF16jPYljbQzcRFe5EHfYY9xE4n4oo6hzRpArFGmoJKKxxBFP7f/58ZWKKsX8yBqJgqJhS8p+Fs6b6+iw==",
"dependencies": {
"@bitcoinerlab/secp256k1": "1.0.2",
"@keystonehq/hw-app-bitcoin": "0.1.2",

View File

@@ -44,10 +44,10 @@
"@phosphor-icons/react": "2.1.7",
"@playwright/test": "1.46.1",
"@react-spring/web": "9.7.3",
"@sats-connect/core": "0.5.2",
"@sats-connect/core": "0.5.3-b107fb6",
"@scure/base": "1.1.9",
"@scure/btc-signer": "1.2.1",
"@secretkeylabs/xverse-core": "38.0.0",
"@secretkeylabs/xverse-core": "38.1.1",
"@stacks/connect": "7.9.0",
"@stacks/stacks-blockchain-api-types": "7.14.1",
"@stacks/transactions": "7.0.2",

View File

@@ -25,7 +25,7 @@ export function PermissionsProvider({ children }: PropsWithChildren) {
throw new Error('Failed to initialize the permissions store', { cause: initError });
}
saveStoreGlobal(newStore);
setStoreGlobal(newStore);
return true;
},

View File

@@ -481,29 +481,22 @@ const useWalletReducer = () => {
dispatchEventAuthorizedConnectedClients(
[
{
type: 'account',
resourceId: permissions.resources.account.makeAccountResourceId(
permissions.utils.account.makeAccountId({
accountId: currentlySelectedAccount.id,
masterPubKey: currentlySelectedAccount.masterPubKey,
networkType: network.type,
}),
),
actions: { read: true },
type: 'wallet',
actions: {
readNetwork: true,
},
{
type: 'account',
resourceId: permissions.resources.account.makeAccountResourceId(
permissions.utils.account.makeAccountId({
accountId: currentlySelectedAccount.id,
masterPubKey: currentlySelectedAccount.masterPubKey,
networkType: changedNetwork.type,
}),
),
actions: { read: true },
resourceId: 'wallet',
},
],
{ type: 'networkChange' },
{
type: 'networkChange',
bitcoin: {
name: changedNetwork.type,
},
stacks: {
name: changedNetwork.type,
},
},
);
}

View File

@@ -41,6 +41,8 @@ export function useMakeHandleAccept({ context, data }: Args) {
origin: context.origin,
};
addClient(client);
const accountId = permissions.utils.account.makeAccountId({
accountId: account.id,
networkType: network.type,
@@ -52,7 +54,7 @@ export function useMakeHandleAccept({ context, data }: Args) {
networkType: network.type,
});
const permission: TPermissions.Store.Permission = {
const accountPermission: TPermissions.Store.Permission = {
type: 'account',
clientId: client.id,
resourceId: resource.id,
@@ -61,9 +63,26 @@ export function useMakeHandleAccept({ context, data }: Args) {
},
};
await addClient(client);
await addResource(resource);
await setPermission(permission);
addResource(resource);
setPermission(accountPermission);
const walletResource: TPermissions.Store.Resource = {
type: 'wallet',
id: 'wallet',
name: 'Wallet',
};
const walletPermission: TPermissions.Store.Permission = {
type: 'wallet',
clientId: client.id,
resourceId: 'wallet',
actions: {
readNetwork: true,
},
};
addResource(walletResource);
setPermission(walletPermission);
if (data.method === 'wallet_requestPermissions')
sendRequestPermissionsSuccessResponseMessage({
@@ -85,6 +104,14 @@ export function useMakeHandleAccept({ context, data }: Args) {
id: accountId,
walletType: account.accountType ?? 'software',
addresses,
network: {
bitcoin: {
name: network.type,
},
stacks: {
name: network.type,
},
},
},
});
}

View File

@@ -118,12 +118,6 @@ export async function sendMessageAuthorizedConnectedClients(
return;
}
if (!store) {
// eslint-disable-next-line no-console
console.warn('Unable to notify connected clients, no permissions store found.');
return;
}
const authorizedClientIds = store.permissions
.filter((storePermission) =>
targetPermissions.some(

View File

@@ -85,10 +85,10 @@ async function loadPermissionsStore(): Promise<
* safely be called multiple times.
*/
export async function initPermissionsStore(): Promise<Result<Permissions.Store.PermissionsStore>> {
const [getStoreError, loadedStore] = await loadPermissionsStore();
const [storeGetError, loadedStore] = await loadPermissionsStore();
if (getStoreError) {
if (getStoreError.name === 'SchemaParseError') {
if (storeGetError) {
if (storeGetError.name === 'SchemaParseError') {
// The store is outdated and needs to be migrated. For now, the store is
// migrated by creating a new store using the current schema and
// overwriting the previous one. As the store becomes more complex, a more
@@ -98,7 +98,7 @@ export async function initPermissionsStore(): Promise<Result<Permissions.Store.P
return success(newStore);
}
if (getStoreError.name === 'StoreNotFoundError') {
if (storeGetError.name === 'StoreNotFoundError') {
// The store doesn't exist yet, so we create a new one.
const newStore = permissions.utils.store.makePermissionsStore();
saveStore(newStore);
@@ -108,7 +108,7 @@ export async function initPermissionsStore(): Promise<Result<Permissions.Store.P
return error({
name: 'InitError',
message: 'Failed to initialize permissions store.',
data: getStoreError,
data: storeGetError,
});
}

View File

@@ -8,6 +8,8 @@ import {
getAccountRequestMessageSchema,
getCurrentPermissionsMethodName,
getCurrentPermissionsRequestMessageSchema,
getNetworkMethodName,
getNetworkRequestMessageSchema,
getWalletTypeMethodName,
getWalletTypeRequestMessageSchema,
renouncePermissionsMethodName,
@@ -21,6 +23,7 @@ import { handleConnect } from '../wallet/connect';
import { handleDisconnect } from '../wallet/disconnect';
import { handleGetAccount } from '../wallet/getAccount';
import { handleGetPermissions } from '../wallet/getCurrentPermissions';
import { handleGetNetwork } from '../wallet/getNetwork';
import { handleGetWalletType } from '../wallet/getWalletType';
import { handleRenouncePermissions } from '../wallet/renouncePermissions';
import { handleRequestPermissions } from '../wallet/requestPermissions';
@@ -73,4 +76,8 @@ export const router: Record<string, Handler> = {
],
validateMessageSchema(getAccountRequestMessageSchema, handleGetAccount),
),
[getNetworkMethodName]: requirePermissions(
[{ type: 'wallet', resourceId: 'wallet', actions: { readNetwork: true } }],
validateMessageSchema(getNetworkRequestMessageSchema, handleGetNetwork),
),
};

View File

@@ -102,6 +102,7 @@ export function requirePermissions(
if (!hasRequiredPermissions) {
sendAccessDeniedResponseMessage({ tabId, messageId: message.id });
return;
}
return handler(message, port);
@@ -112,7 +113,7 @@ export function validateMessageSchema<
const TSchema extends v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>,
>(
schema: TSchema,
handler: (message: v.InferOutput<TSchema>, port: chrome.runtime.Port) => Promise<void>,
handler: (message: v.InferOutput<TSchema>, port: chrome.runtime.Port) => Promise<void> | void,
) {
return async (message: RpcRequestMessage, port: chrome.runtime.Port) => {
const parseResult = v.safeParse(schema, message);

View File

@@ -46,8 +46,19 @@ export function sendGetCurrentPermissionsSuccessResponseMessage({
sendRpcResponse(tabId, makeRpcSuccessResponse(messageId, result));
}
type GetAccountSuccessArgs = BaseArgs & {
result: Return<'wallet_getAccount'>;
};
export function sendGetAccountSuccessResponseMessage({
tabId,
messageId,
result,
}: GetAccountSuccessArgs) {
sendRpcResponse(tabId, makeRpcSuccessResponse(messageId, result));
}
type ConnectSuccessArgs = BaseArgs & {
result: ConnectResult;
result: Return<'wallet_connect'>;
};
export function sendConnectSuccessResponseMessage({
tabId,
@@ -56,3 +67,14 @@ export function sendConnectSuccessResponseMessage({
}: ConnectSuccessArgs) {
sendRpcResponse(tabId, makeRpcSuccessResponse(messageId, result));
}
type GetNetworkSuccessArgs = BaseArgs & {
result: Return<'wallet_getNetwork'>;
};
export function sendGetNetworkSuccessResponseMessage({
tabId,
messageId,
result,
}: GetNetworkSuccessArgs) {
sendRpcResponse(tabId, makeRpcSuccessResponse(messageId, result));
}

View File

@@ -13,8 +13,14 @@ import { sendInternalErrorMessage } from '../responseMessages/errors';
import { sendConnectSuccessResponseMessage } from '../responseMessages/wallet';
export const handleConnect = async (message: ConnectRequestMessage, port: chrome.runtime.Port) => {
// Check if the user already has account read permissions, and if so, return
// the account data without opening the popup.
// Check if the user already has account & network read permissions, and if
// so, return the account data without opening the popup.
//
// Note: the checks performed in this file for the default permissions that
// `wallet_connect` is expected to grant is decoupled from and needs to be
// manually kept in sync with those granted in
// `src/app/screens/connect/connectionRequest/hooks.ts`.
const [error, store] = await initPermissionsStore();
if (error) {
sendInternalErrorMessage({
@@ -75,8 +81,14 @@ export const handleConnect = async (message: ConnectRequestMessage, port: chrome
resourceId,
actions: { read: true },
});
const hasNetworkReadPermissions = permissions.utils.store.hasPermission(store, {
type: 'wallet',
clientId,
resourceId: 'wallet',
actions: { readNetwork: true },
});
if (!hasAccountReadPermissions) {
if (!hasAccountReadPermissions || !hasNetworkReadPermissions) {
openPopup({
path: RequestsRoutes.ConnectionRequest,
data: message,
@@ -95,6 +107,14 @@ export const handleConnect = async (message: ConnectRequestMessage, port: chrome
id: accountId,
walletType: account.accountType ?? 'software',
addresses,
network: {
bitcoin: {
name: network.type,
},
stacks: {
name: network.type,
},
},
};
sendConnectSuccessResponseMessage({
tabId: getTabIdFromPort(port),

View File

@@ -1,12 +1,12 @@
/* eslint-disable import/prefer-default-export */
import { getTabIdFromPort } from '@common/utils';
import getSelectedAccount, { embellishAccountWithDetails } from '@common/utils/getSelectedAccount';
import { type ConnectResult, type GetAccountRequestMessage } from '@sats-connect/core';
import { type GetAccountRequestMessage, type GetAccountResult } from '@sats-connect/core';
import { permissions } from '@secretkeylabs/xverse-core';
import rootStore from '@stores/index';
import { accountPurposeAddresses } from '../btc/getAddresses/utils';
import { sendInternalErrorMessage } from '../responseMessages/errors';
import { sendConnectSuccessResponseMessage } from '../responseMessages/wallet';
import { sendGetAccountSuccessResponseMessage } from '../responseMessages/wallet';
export async function handleGetAccount(
message: GetAccountRequestMessage,
@@ -47,12 +47,12 @@ export async function handleGetAccount(
const embellishedAccount = embellishAccountWithDetails(account, btcPaymentAddressType);
const addresses = accountPurposeAddresses(embellishedAccount, { type: 'all' });
const result: ConnectResult = {
const result: GetAccountResult = {
id: accountId,
walletType: account.accountType ?? 'software',
addresses,
};
sendConnectSuccessResponseMessage({
sendGetAccountSuccessResponseMessage({
tabId: getTabIdFromPort(port),
messageId: message.id,
result,

View File

@@ -0,0 +1,24 @@
import { getTabIdFromPort } from '@common/utils';
import type { GetNetworkRequestMessage, GetNetworkResult } from '@sats-connect/core';
import rootStore from '@stores/index';
import { sendGetNetworkSuccessResponseMessage } from '../responseMessages/wallet';
export function handleGetNetwork(message: GetNetworkRequestMessage, port: chrome.runtime.Port) {
const { network } = rootStore.store.getState().walletState;
const result: GetNetworkResult = {
bitcoin: {
name: network.type,
},
stacks: {
// QUESTION: Do we need to add a stacks network names we could use here?
name: network.type,
},
};
sendGetNetworkSuccessResponseMessage({
tabId: getTabIdFromPort(port),
messageId: message.id,
result,
});
}