feat: add rpc methods

This commit is contained in:
kyranjamie
2023-03-07 18:41:27 +01:00
committed by kyranjamie
parent e667284963
commit 8206122b19
38 changed files with 597 additions and 188 deletions

View File

@@ -217,6 +217,7 @@
"@babel/core": "7.20.12",
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "7.18.6",
"@btckit/types": "0.0.11",
"@emotion/babel-plugin": "11.10.5",
"@emotion/babel-preset-css-prop": "11.10.0",
"@emotion/cache": "11.10.5",

View File

@@ -0,0 +1,32 @@
<svg width="134" height="132" viewBox="0 0 134 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2265_80078)">
<path d="M74.2178 88.9042V43.0576C74.2416 39.2652 75.7811 35.6354 78.5017 32.9573C81.2223 30.2792 84.904 28.7694 88.7463 28.756H133.648C133.059 20.9393 129.497 13.6316 123.677 8.29839C117.857 2.96518 110.21 0.000882672 102.268 0L31.4711 0C23.5305 0.00386362 15.8847 2.96917 10.0656 8.30179C4.2465 13.6344 0.683899 20.9404 0.0916138 28.756H44.9931C48.8444 28.7694 52.5338 30.2863 55.2558 32.9754C57.9779 35.6645 59.5114 39.3073 59.5216 43.1085V88.9551C59.4945 92.7453 57.9534 96.3719 55.2332 99.0472C52.513 101.723 48.8333 103.231 44.9931 103.244H0.0916138C0.683899 111.06 4.2465 118.366 10.0656 123.698C15.8847 129.031 23.5305 131.996 31.4711 132H102.268C110.21 131.999 117.857 129.035 123.677 123.702C129.497 118.368 133.059 111.061 133.648 103.244H88.7463C84.8973 103.231 81.2098 101.715 78.4882 99.0291C75.7665 96.3428 74.2314 92.7032 74.2178 88.9042Z" fill="url(#paint0_linear_2265_80078)"/>
<path d="M59.5203 88.9043V43.0577C59.4965 39.2653 57.957 35.6355 55.2364 32.9574C52.5158 30.2793 48.8342 28.7695 44.9918 28.7561H0.0903191C0.0903191 29.5202 0 30.2843 0 31.0612V64.7202H35.8438L28.3086 57.0791C28.0673 56.8376 27.9332 56.5114 27.9356 56.1723C27.938 55.8331 28.0768 55.5088 28.3215 55.2707C28.5662 55.0326 28.8967 54.9002 29.2403 54.9026C29.5839 54.9049 29.9124 55.042 30.1537 55.2835L39.8565 65.0386C40.0969 65.2772 40.2317 65.6 40.2317 65.9364C40.2317 66.2729 40.0969 66.5956 39.8565 66.8342L30.1537 76.5894C29.9124 76.8309 29.5839 76.9679 29.2403 76.9703C28.8967 76.9727 28.5662 76.8402 28.3215 76.6021C28.0768 76.364 27.938 76.0397 27.9356 75.7006C27.9332 75.3614 28.0673 75.0352 28.3086 74.7937L35.8438 67.1526H0V100.939C0 101.716 0 102.48 0.0903191 103.244H44.9918C48.8408 103.231 52.5283 101.716 55.25 99.0292C57.9717 96.3429 59.5067 92.7033 59.5203 88.9043Z" fill="url(#paint1_linear_2265_80078)"/>
<path d="M105.441 74.8574C105.682 75.1006 105.816 75.4284 105.812 75.7687C105.809 76.1091 105.668 76.434 105.422 76.6722C105.175 76.9103 104.843 77.042 104.498 77.0385C104.154 77.0349 103.824 76.8962 103.583 76.6531L93.8803 66.8979C93.64 66.6593 93.5051 66.3365 93.5051 66.0001C93.5051 65.6636 93.64 65.3409 93.8803 65.1023L103.583 55.3471C103.824 55.1039 104.154 54.9653 104.498 54.9617C104.843 54.9581 105.175 55.0899 105.422 55.328C105.668 55.5661 105.809 55.8911 105.812 56.2314C105.816 56.5718 105.682 56.8996 105.441 57.1428L97.8931 64.7839H133.737V31.0612C133.737 30.2843 133.737 29.5202 133.647 28.7561H88.745C84.8938 28.7695 81.2044 30.2864 78.4823 32.9755C75.7603 35.6646 74.2268 39.3074 74.2166 43.1086V88.9552C74.2437 92.7454 75.7847 96.3719 78.505 99.0473C81.2252 101.723 84.9049 103.231 88.745 103.244H133.647C133.647 102.48 133.737 101.716 133.737 100.939V67.28H97.8931L105.441 74.8574Z" fill="#EFEFF2"/>
<path d="M105.441 74.8574C105.682 75.1006 105.816 75.4284 105.812 75.7687C105.809 76.1091 105.668 76.434 105.422 76.6722C105.175 76.9103 104.843 77.042 104.498 77.0385C104.154 77.0349 103.824 76.8962 103.583 76.6531L93.8803 66.8979C93.64 66.6593 93.5051 66.3365 93.5051 66.0001C93.5051 65.6636 93.64 65.3409 93.8803 65.1023L103.583 55.3471C103.824 55.1039 104.154 54.9653 104.498 54.9617C104.843 54.9581 105.175 55.0899 105.422 55.328C105.668 55.5661 105.809 55.8911 105.812 56.2314C105.816 56.5718 105.682 56.8996 105.441 57.1428L97.8931 64.7839H133.737V31.0612C133.737 30.2843 133.737 29.5202 133.647 28.7561H88.745C84.8938 28.7695 81.2044 30.2864 78.4823 32.9755C75.7603 35.6646 74.2268 39.3074 74.2166 43.1086V88.9552C74.2437 92.7454 75.7847 96.372 78.505 99.0473C81.2252 101.723 84.9049 103.231 88.745 103.244H133.647C133.647 102.48 133.737 101.716 133.737 100.939V67.28H97.8931L105.441 74.8574Z" fill="url(#paint2_linear_2265_80078)"/>
<path d="M114.375 49.1595C114.603 48.9342 114.785 48.666 114.908 48.3706C115.032 48.0752 115.096 47.7583 115.096 47.4383C115.096 47.1183 115.032 46.8014 114.908 46.506C114.785 46.2106 114.603 45.9425 114.375 45.7171C114.148 45.4899 113.878 45.3095 113.581 45.1864C113.284 45.0634 112.966 45 112.644 45C112.322 45 112.003 45.0634 111.706 45.1864C111.409 45.3095 111.139 45.4899 110.913 45.7171L92.5774 64.2869C92.1233 64.7411 91.8684 65.3555 91.8684 65.996C91.8684 66.6364 92.1233 67.2509 92.5774 67.7051L110.913 86.2749C111.368 86.7346 111.989 86.9954 112.639 86.9999C113.288 87.0045 113.912 86.7524 114.375 86.2991C114.837 85.8458 115.099 85.2285 115.104 84.5829C115.109 83.9373 114.855 83.3164 114.399 82.8567L100.16 68.3111L167.868 68.3111L167.868 63.4626L100.136 63.4626L114.375 49.1595Z" fill="white"/>
<path d="M18.3621 82.8405C18.1335 83.0658 17.9522 83.334 17.8284 83.6294C17.7046 83.9248 17.6409 84.2417 17.6409 84.5617C17.6409 84.8817 17.7046 85.1986 17.8284 85.494C17.9522 85.7894 18.1335 86.0575 18.3621 86.2829C18.5887 86.5101 18.8584 86.6905 19.1555 86.8136C19.4526 86.9366 19.7713 87 20.0932 87C20.415 87 20.7337 86.9366 21.0308 86.8136C21.328 86.6905 21.5976 86.5101 21.8243 86.2829L40.1594 67.7131C40.6135 67.2589 40.8684 66.6445 40.8684 66.004C40.8684 65.3636 40.6135 64.7491 40.1594 64.2949L21.8243 45.7251C21.3684 45.2654 20.7475 45.0046 20.0982 45.0001C19.4489 44.9955 18.8244 45.2476 18.3621 45.7009C17.8997 46.1542 17.6374 46.7715 17.6328 47.4171C17.6283 48.0627 17.8818 48.6836 18.3377 49.1433L32.5767 63.6889H-35.1316V68.5374H32.601L18.3621 82.8405Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_2265_80078" x1="24.0841" y1="23.7129" x2="117.414" y2="118.143" gradientUnits="userSpaceOnUse">
<stop stop-color="#353535"/>
<stop offset="0.26" stop-color="#303030"/>
<stop offset="0.56" stop-color="#222222"/>
<stop offset="0.87" stop-color="#0B0B0B"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_2265_80078" x1="0.468421" y1="106.459" x2="55.7197" y2="66.4943" gradientUnits="userSpaceOnUse">
<stop stop-color="#5546FF"/>
<stop offset="0.35" stop-color="#7D75DD"/>
<stop offset="0.72" stop-color="#A3A0BE"/>
<stop offset="0.91" stop-color="#B1B1B2"/>
</linearGradient>
<linearGradient id="paint2_linear_2265_80078" x1="92.3965" y1="50.4059" x2="133.184" y2="91.7298" gradientUnits="userSpaceOnUse">
<stop stop-color="#5845FF"/>
<stop offset="1" stop-color="#803CF3"/>
</linearGradient>
<clipPath id="clip0_2265_80078">
<rect width="133.737" height="132" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

View File

@@ -18,7 +18,7 @@ import { useKeyActions } from '@app/common/hooks/use-key-actions';
import { useWalletType } from '@app/common/use-wallet-type';
import {
useAllBitcoinNativeSegWitNetworksByAccount,
useCurrentBitcoinNativeSegwitAddressIndexKeychain,
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import {
useAllBitcoinTaprootNetworksByAccount,
@@ -41,7 +41,7 @@ export function useFinishAuthRequest() {
const deriveNativeSegWitAccountAtIndex = useAllBitcoinNativeSegWitNetworksByAccount();
const deriveTaprootAccountAtIndex = useAllBitcoinTaprootNetworksByAccount();
const currentBitcoinNativeSegwitAddressIndexKeychain =
useCurrentBitcoinNativeSegwitAddressIndexKeychain();
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain();
const currentBitcoinTaprootAddressIndexKeychain = useCurrentTaprootAddressIndexKeychain();
return useCallback(

View File

@@ -0,0 +1,16 @@
import { useState } from 'react';
interface FaviconProps {
origin: string;
}
export function Favicon({ origin }: FaviconProps) {
const [hasError, setHasError] = useState(false);
if (hasError) return null;
return (
<img
src={`http://www.google.com/s2/favicons?domain=${origin}`}
onError={() => setHasError(true)}
/>
);
}

View File

@@ -10,7 +10,7 @@ export function ContainerLayout(props: ContainerLayoutProps) {
return (
<Flex flexDirection="column" flexGrow={1} width="100%" background={color('bg')}>
{header || null}
<Flex className="main-content" flexGrow={1} pb="loose" position="relative" width="100%">
<Flex className="main-content" flexGrow={1} position="relative" width="100%">
{children}
</Flex>
</Flex>

View File

@@ -1,5 +1,7 @@
import { Outlet, useNavigate } from 'react-router-dom';
import { Button } from '@stacks/ui';
import { RouteUrls } from '@shared/route-urls';
import { useTrackFirstDeposit } from '@app/common/hooks/analytics/transactions-analytics.hooks';
@@ -13,6 +15,7 @@ import { InAppMessages } from '@app/features/hiro-messages/in-app-messages';
import { SuggestedFirstSteps } from '@app/features/suggested-first-steps/suggested-first-steps';
import { HomeActions } from '@app/pages/home/components/home-actions';
import { StacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.models';
import { useAppPermissions } from '@app/store/app-permissions/app-permissions.slice';
import { CurrentAccount } from './components/account-area';
import { HomeTabs } from './components/home-tabs';
@@ -42,12 +45,25 @@ function HomeContainer({ account }: HomeContainerProps) {
if (decodedAuthRequest) navigate(RouteUrls.ChooseAccount);
});
const perm = useAppPermissions();
return (
<HomeLayout
suggestedFirstSteps={<SuggestedFirstSteps />}
currentAccount={<CurrentAccount />}
actions={<HomeActions />}
>
<Button
onClick={() => {
perm.hasRequestedAccounts(
`example${Math.ceil(Math.random() * 1000)
.toPrecision(4)
.replace('.', '')}.com`
);
}}
>
lskdjflksd
</Button>
<HomeTabs balances={<BalancesList address={account.address} />} activity={<ActivityList />} />
<Outlet />
</HomeLayout>

View File

@@ -0,0 +1,56 @@
import ConnectionIllustration from '@assets/images/connect-arrows-facing-each-other.svg';
import HiroLogoInBox from '@assets/images/hiro-logo-white-box.png';
import { Box, Button, Flex, Text } from '@stacks/ui';
import { Favicon } from '@app/components/favicon';
import { Flag } from '@app/components/layout/flag';
import { Caption } from '@app/components/typography';
interface RequestAccountsLayoutProps {
requester: string;
onUserApproveRequestAccounts(): void;
}
export function RequestAccountsLayout(props: RequestAccountsLayoutProps) {
const { requester, onUserApproveRequestAccounts } = props;
return (
<Flex flexDirection="column" height="100vh" width="100%">
<Flex
flex={1}
flexDirection="column"
justifyContent="center"
textAlign="center"
alignItems="center"
mx="extra-loose"
>
<Box mt="extra-loose">
<img src={ConnectionIllustration} width="132" />
</Box>
<Text as="h1" mt="base" fontSize="24px" fontWeight={500} lineHeight="36px">
Connect your account to {requester}
</Text>
<Flag img={<Favicon origin={requester} />} mt="base">
<Caption>{requester}</Caption>
</Flag>
<Button mt="extra-loose" onClick={() => onUserApproveRequestAccounts()} width="100%">
<Flag align="middle" img={<img src={HiroLogoInBox} width="16px" />}>
Connect to Hiro Wallet
</Flag>
</Button>
</Flex>
<Flex
backgroundColor="#F5F5F7"
px="loose"
py="base"
lineHeight="20px"
textAlign="center"
alignSelf="bottom"
>
<Text fontSize="14px" color="#74777D">
By connecting you give permission to {requester} to see all addresses linked to this
account
</Text>
</Flex>
</Flex>
);
}

View File

@@ -0,0 +1,20 @@
import { RequestAccountsLayout } from './components/request-accounts.layout';
import { useRequestAccounts } from './use-request-accounts';
export function RequestAccounts() {
const { origin, onUserApproveRequestAccounts } = useRequestAccounts();
if (origin === null) {
window.close();
throw new Error('Origin is null');
}
const requester = new URL(origin).host;
return (
<RequestAccountsLayout
requester={requester}
onUserApproveRequestAccounts={onUserApproveRequestAccounts}
/>
);
}

View File

@@ -0,0 +1,66 @@
import { useMemo } from 'react';
import { BtcAddress } from '@btckit/types';
import { logger } from '@shared/logger';
import { makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import { useCurrentBtcNativeSegwitAccountAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentBtcTaprootAccountAddressIndexZeroPayment } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks';
import { useAppPermissions } from '@app/store/app-permissions/app-permissions.slice';
function useRpcRequestParams() {
const defaultParams = useDefaultRequestParams();
return useMemo(
() => ({
...defaultParams,
requestId: initialSearchParams.get('requestId') ?? '',
}),
[defaultParams]
);
}
export function useRequestAccounts() {
const analytics = useAnalytics();
const permissions = useAppPermissions();
const { tabId, origin, requestId } = useRpcRequestParams();
const nativeSegwitAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
const taprootPayment = useCurrentBtcTaprootAccountAddressIndexZeroPayment();
const taprootAddressResponse: BtcAddress = {
type: 'p2tr',
address: taprootPayment.address,
};
const nativeSegwitAddressResponse: BtcAddress = {
type: 'p2wpkh',
address: nativeSegwitAddress,
};
return {
origin,
onUserApproveRequestAccounts() {
if (!tabId || !origin) {
logger.error('Cannot give app accounts: missing tabId, origin');
return;
}
void analytics.track('user_approved_request_accounts', { origin });
permissions.hasRequestedAccounts(origin);
chrome.tabs.sendMessage(
tabId,
makeRpcSuccessResponse('getAddresses', {
id: requestId,
result: {
addresses: [nativeSegwitAddressResponse, taprootAddressResponse],
},
})
);
window.close();
},
};
}

View File

@@ -10,7 +10,7 @@ import { useGetUtxosByAddressQuery } from '@app/query/bitcoin/address/utxos-by-a
import { useBitcoinFeeRate } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { useBitcoinLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import {
useCurrentBitcoinNativeSegwitAddressIndexKeychain,
useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain,
useCurrentBtcNativeSegwitAccountAddressIndexZero,
useSignBitcoinNativeSegwitTx,
} from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
@@ -18,7 +18,7 @@ import {
export function useGenerateSignedBitcoinTx() {
const currentAccountBtcAddress = useCurrentBtcNativeSegwitAccountAddressIndexZero();
const { data: utxos } = useGetUtxosByAddressQuery(currentAccountBtcAddress);
const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexKeychain();
const currentAddressIndexKeychain = useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain();
const signTx = useSignBitcoinNativeSegwitTx();
const networkMode = useBitcoinLibNetworkConfig();
const { data: feeRate } = useBitcoinFeeRate();

View File

@@ -1,7 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import * as yup from 'yup';
import { Prettify } from '@app/common/type-utils';
import { Prettify } from '@shared/utils/type-utils';
import { QueryPrefixes } from '@app/query/query-prefixes';
/**

View File

@@ -63,7 +63,7 @@ export function useNextFreshTaprootAddressQuery(accIndex?: number) {
// Check shouldn't be necessary, but just in case
if (emptyAddress.utxos.length !== 0) throw new Error('Address found not empty');
setHighestKnownAccountActivity(emptyAddress.index - 1);
setHighestKnownAccountActivity(Math.max(0, emptyAddress.index - 1));
return emptyAddress.address;
},
{

View File

@@ -1,7 +1,8 @@
import { useQueries, useQuery } from '@tanstack/react-query';
import * as yup from 'yup';
import { Prettify } from '@app/common/type-utils';
import { Prettify } from '@shared/utils/type-utils';
import { AppUseQueryConfig } from '@app/query/query-config';
import { QueryPrefixes } from '@app/query/query-prefixes';

View File

@@ -32,6 +32,7 @@ import { ReceiveModal } from '@app/pages/receive-tokens/receive-modal';
import { ReceiveStxModal } from '@app/pages/receive-tokens/receive-stx';
import { ReceiveCollectibleModal } from '@app/pages/receive/receive-collectible/receive-collectible-modal';
import { ReceiveCollectibleOrdinal } from '@app/pages/receive/receive-collectible/receive-collectible-oridinal';
import { RequestAccounts } from '@app/pages/request-accounts/request-accounts';
import { SelectNetwork } from '@app/pages/select-network/select-network';
import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error';
import { SendInscription } from '@app/pages/send/ordinal-inscription/send-inscription-container';
@@ -246,6 +247,15 @@ function AppRoutesAfterUserHasConsented() {
{settingsModalRoutes}
</Route>
<Route
path={RouteUrls.RequestTapootAddress}
element={
<AccountGate>
<RequestAccounts />
</AccountGate>
}
/>
{/* Catch-all route redirects to onboarding */}
<Route path="*" element={<Navigate replace to={RouteUrls.Onboarding} />} />
</Route>

View File

@@ -1,9 +1,12 @@
import { HDKey } from '@scure/bip32';
import { DerivationPathDepth } from '@shared/crypto/derivation-path.utils';
interface SoftwareBitcoinAccount {
type: 'software';
xpub: string;
index: number;
keychain: HDKey;
}
// TODO: complete with bitcoin ledger support
@@ -25,9 +28,14 @@ export const tempHardwareAccountForTesting: HardwareBitcoinAccount = {
};
export function formatBitcoinAccount(keychain: HDKey) {
if (keychain.depth !== DerivationPathDepth.Account)
throw new Error('Can only format from account keychain');
return (index: number): SoftwareBitcoinAccount => ({
type: 'software',
index,
// The rationate for wrapping the keychain is so we pass around the xpub
// rather than the private-key containing HDKey
xpub: keychain.publicExtendedKey,
keychain: HDKey.fromExtendedKey(keychain.publicExtendedKey),
});
}

View File

@@ -7,7 +7,7 @@ import { deriveAddressIndexZeroFromAccount } from '@shared/crypto/bitcoin/bitcoi
import { deriveTaprootAccountFromRootKeychain } from '@shared/crypto/bitcoin/p2tr-address-gen';
import {
deriveNativeSegWitAccountKeychain,
getNativeSegWitAddressIndexDetails,
getNativeSegWitAddressIndexFromKeychain,
} from '@shared/crypto/bitcoin/p2wpkh-address-gen';
import { mnemonicToRootNode } from '@app/common/keychain/keychain';
@@ -38,7 +38,7 @@ export function getNativeSegwitMainnetAddressFromMnemonic(secretKey: string) {
return (accountIndex: number) => {
const rootNode = mnemonicToRootNode(secretKey);
const account = deriveNativeSegWitAccountKeychain(rootNode, 'mainnet')(accountIndex);
return getNativeSegWitAddressIndexDetails(
return getNativeSegWitAddressIndexFromKeychain(
deriveAddressIndexZeroFromAccount(account),
'mainnet'
);

View File

@@ -31,6 +31,22 @@ function useNativeSegWitCurrentNetworkAccountKeychain() {
);
}
export function useCurrentBitcoinNativeSegwitAccountPublicKeychain() {
const { xpub } = useCurrentBitcoinNativeSegwitAccountInfo();
if (!xpub) return; // TODO: Revisit this return early
const keychain = HDKey.fromExtendedKey(xpub);
if (!keychain?.publicKey) throw new Error('No public key for given keychain');
if (!keychain.pubKeyHash) throw new Error('No pub key hash for given keychain');
return keychain;
}
// Concept of current address index won't exist with privacy mode
export function useCurrentBitcoinNativeSegwitAddressIndexPublicKeychain() {
const keychain = useCurrentBitcoinNativeSegwitAccountPublicKeychain();
if (!keychain) return; // TODO: Revisit this return early
return deriveAddressIndexZeroFromAccount(keychain);
}
export function useAllBitcoinNativeSegWitNetworksByAccount() {
const mainnetKeychainAtAccount = useSelector(selectMainnetNativeSegWitKeychain);
const testnetKeychainAtAccount = useSelector(selectTestnetNativeSegWitKeychain);
@@ -54,7 +70,7 @@ export function useAllBitcoinNativeSegWitNetworksByAccount() {
);
}
function useBitcoinNativeSegwitAccount(index: number) {
function useBitcoinNativeSegwitAccountInfo(index: number) {
const keychain = useNativeSegWitCurrentNetworkAccountKeychain();
return useMemo(() => {
// TODO: Remove with bitcoin Ledger integration
@@ -63,9 +79,9 @@ function useBitcoinNativeSegwitAccount(index: number) {
}, [keychain, index]);
}
function useCurrentBitcoinNativeSegwitAccount() {
export function useCurrentBitcoinNativeSegwitAccountInfo() {
const currentAccountIndex = useCurrentAccountIndex();
return useBitcoinNativeSegwitAccount(currentAccountIndex);
return useBitcoinNativeSegwitAccountInfo(currentAccountIndex);
}
function useDeriveNativeSegWitAccountIndexAddressIndexZero(xpub: string) {
@@ -81,31 +97,15 @@ function useDeriveNativeSegWitAccountIndexAddressIndexZero(xpub: string) {
}
export function useCurrentBtcNativeSegwitAccountAddressIndexZero() {
const { xpub } = useCurrentBitcoinNativeSegwitAccount();
const { xpub } = useCurrentBitcoinNativeSegwitAccountInfo();
return useDeriveNativeSegWitAccountIndexAddressIndexZero(xpub)?.address as string;
}
export function useBtcNativeSegwitAccountIndexAddressIndexZero(accountIndex: number) {
const { xpub } = useBitcoinNativeSegwitAccount(accountIndex);
const { xpub } = useBitcoinNativeSegwitAccountInfo(accountIndex);
return useDeriveNativeSegWitAccountIndexAddressIndexZero(xpub)?.address as string;
}
function useCurrentBitcoinNativeSegwitAccountKeychain() {
const { xpub } = useCurrentBitcoinNativeSegwitAccount();
if (!xpub) return; // TODO: Revisit this return early
const keychain = HDKey.fromExtendedKey(xpub);
if (!keychain?.publicKey) throw new Error('No public key for given keychain');
if (!keychain.pubKeyHash) throw new Error('No pub key hash for given keychain');
return keychain;
}
// Concept of current address index won't exist with privacy mode
export function useCurrentBitcoinNativeSegwitAddressIndexKeychain() {
const keychain = useCurrentBitcoinNativeSegwitAccountKeychain();
if (!keychain) return; // TODO: Revisit this return early
return deriveAddressIndexZeroFromAccount(keychain);
}
export function useSignBitcoinNativeSegwitTx() {
const index = useCurrentAccountIndex();
const keychain = useNativeSegWitCurrentNetworkAccountKeychain()?.(index);

View File

@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import * as btc from '@scure/btc-signer';
@@ -17,9 +17,10 @@ import { whenNetwork } from '@app/common/utils';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { useCurrentAccountIndex } from '../../account';
import { formatBitcoinAccount, tempHardwareAccountForTesting } from './bitcoin-account.models';
import { selectMainnetTaprootKeychain, selectTestnetTaprootKeychain } from './bitcoin-keychain';
function useTaprootCurrentNetworkAccountKeychain() {
function useTaprootCurrentNetworkAccountPrivateKeychain() {
const network = useCurrentNetwork();
return useSelector(
whenNetwork(network.chain.bitcoin.network)({
@@ -35,11 +36,9 @@ export function useCurrentTaprootAccountKeychain() {
}
export function useTaprootAccountKeychain(accountIndex: number) {
const accountKeychain = useTaprootCurrentNetworkAccountKeychain();
const accountKeychain = useTaprootCurrentNetworkAccountPrivateKeychain();
if (!accountKeychain) return; // TODO: Revisit this return early
const keychain = accountKeychain(accountIndex);
if (!keychain) throw new Error('No account keychain found');
return keychain;
return accountKeychain(accountIndex);
}
// Concept of current address index won't exist with privacy mode
@@ -49,9 +48,44 @@ export function useCurrentTaprootAddressIndexKeychain() {
return deriveAddressIndexZeroFromAccount(keychain);
}
function useBitcoinTaprootAccountInfo(index: number) {
const keychain = useTaprootCurrentNetworkAccountPrivateKeychain();
return useMemo(() => {
// TODO: Remove with bitcoin Ledger integration
if (isUndefined(keychain)) return tempHardwareAccountForTesting;
return formatBitcoinAccount(keychain(index))(index);
}, [keychain, index]);
}
export function useCurrentBitcoinTaprootAccountInfo() {
const currentAccountIndex = useCurrentAccountIndex();
return useBitcoinTaprootAccountInfo(currentAccountIndex);
}
export function useDeriveTaprootAccountIndexAddressIndexZero(xpub: string) {
const network = useCurrentNetwork();
return useMemo(
() =>
deriveTaprootReceiveAddressIndex({
xpub,
index: 0,
network: network.chain.bitcoin.network,
}),
[xpub, network]
);
}
export function useCurrentBtcTaprootAccountAddressIndexZeroPayment() {
const { xpub } = useCurrentBitcoinTaprootAccountInfo();
const payment = useDeriveTaprootAccountIndexAddressIndexZero(xpub);
if (!payment?.address) throw new Error('No address found');
// Creating new object to have known property types
return { address: payment.address, type: payment.type };
}
export function useSignBitcoinTaprootTx() {
const index = useCurrentAccountIndex();
const keychain = useTaprootCurrentNetworkAccountKeychain()?.(index);
const keychain = useTaprootCurrentNetworkAccountPrivateKeychain()?.(index);
return useCallback(
(tx: btc.Transaction) => {
@@ -69,7 +103,7 @@ interface UseSignBitcoinTaprootInputAtIndexArgs {
}
export function useSignBitcoinTaprootInputAtIndex() {
const index = useCurrentAccountIndex();
const keychain = useTaprootCurrentNetworkAccountKeychain()?.(index);
const keychain = useTaprootCurrentNetworkAccountPrivateKeychain()?.(index);
return useCallback(
({ allowedSighash, idx, tx }: UseSignBitcoinTaprootInputAtIndexArgs) => {

View File

@@ -0,0 +1,41 @@
import { useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
interface AppPermission {
origin: string;
// Very simple permission system. If property exists with date, user
// has given permission
requestedAccounts?: string;
}
const appPermissionsAdapter = createEntityAdapter<AppPermission>({
selectId: permission => permission.origin,
});
const initialState = appPermissionsAdapter.getInitialState();
export const appPermissionsSlice = createSlice({
name: 'appPermissions',
initialState,
reducers: { updatePermission: appPermissionsAdapter.upsertOne },
});
export function useAppPermissions() {
const dispatch = useDispatch();
return useMemo(
() => ({
hasRequestedAccounts(origin: string) {
dispatch(
appPermissionsSlice.actions.updatePermission({
origin,
requestedAccounts: new Date().toISOString(),
})
);
},
}),
[dispatch]
);
}

View File

@@ -19,6 +19,7 @@ import { IS_DEV_ENV } from '@shared/environment';
import { persistConfig } from '@shared/storage';
import { analyticsSlice } from './analytics/analytics.slice';
import { appPermissionsSlice } from './app-permissions/app-permissions.slice';
import { stxChainSlice } from './chains/stx-chain.slice';
import { inMemoryKeySlice } from './in-memory-key/in-memory-key.slice';
import { keySlice } from './keys/key.slice';
@@ -30,6 +31,7 @@ import { broadcastActionTypeToOtherFramesMiddleware } from './utils/broadcast-ac
export interface RootState {
analytics: ReturnType<typeof analyticsSlice.reducer>;
appPermissions: ReturnType<typeof appPermissionsSlice.reducer>;
chains: {
stx: ReturnType<typeof stxChainSlice.reducer>;
};
@@ -43,6 +45,7 @@ export interface RootState {
const appReducer = combineReducers({
analytics: analyticsSlice.reducer,
appPermissions: appPermissionsSlice.reducer,
chains: combineReducers({
stx: stxChainSlice.reducer,
}),

View File

@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react';
import { logger } from '@shared/logger';
import { CONTENT_SCRIPT_PORT, LegacyMessageFromContentScript } from '@shared/message-types';
import { RouteUrls } from '@shared/route-urls';
import { WalletRequests } from '@shared/rpc/rpc-methods';
import { initSentry } from '@shared/utils/analytics';
import { warnUsersAboutDevToolsDangers } from '@shared/utils/dev-tools-warning-log';
@@ -11,8 +12,9 @@ import { initContextMenuActions } from './init-context-menus';
import {
handleLegacyExternalMethodFormat,
isLegacyMessage,
} from './legacy-external-message-handler';
import { internalBackgroundMessageHandler } from './message-handler';
} from './messaging/legacy-external-message-handler';
import { internalBackgroundMessageHandler } from './messaging/message-handler';
import { rpcMessageHander } from './messaging/rpc-message-handler';
initSentry();
initContextMenuActions();
@@ -37,7 +39,7 @@ chrome.runtime.onConnect.addListener(port =>
Sentry.wrap(() => {
if (port.name !== CONTENT_SCRIPT_PORT) return;
port.onMessage.addListener((message: LegacyMessageFromContentScript, port) => {
port.onMessage.addListener((message: LegacyMessageFromContentScript | WalletRequests, port) => {
if (!port.sender?.tab?.id)
return logger.error('Message reached background script without a corresponding tab');
@@ -47,6 +49,7 @@ chrome.runtime.onConnect.addListener(port =>
if (!originUrl)
return logger.error('Message reached background script without a corresponding origin');
// Legacy JWT format messages
if (isLegacyMessage(message)) {
void handleLegacyExternalMethodFormat(message, port);
return;
@@ -55,6 +58,7 @@ chrome.runtime.onConnect.addListener(port =>
// TODO:
// Here we'll handle all messages using the rpc style comm method
// For now all messages are handled as legacy format
void rpcMessageHander(message, port);
});
})
);

View File

@@ -3,28 +3,16 @@ import { formatMessageSigningResponse } from '@shared/actions/finalize-message-s
import { formatProfileUpdateResponse } from '@shared/actions/finalize-profile-update';
import { formatPsbtResponse } from '@shared/actions/finalize-psbt';
import { formatTxSignatureResponse } from '@shared/actions/finalize-tx-signature';
import {
ExternalMethods,
InternalMethods,
LegacyMessageFromContentScript,
LegacyMessageToContentScript,
} from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { ExternalMethods, LegacyMessageFromContentScript } from '@shared/message-types';
import { RouteUrls } from '@shared/route-urls';
import { getCoreApiUrl, getPayloadFromToken } from '@shared/utils/requests';
import { popupCenter } from './popup-center';
const IS_TEST_ENV = process.env.TEST_ENV === 'true';
//
// Playwright does not currently support Chrome extension popup testing:
// https://github.com/microsoft/playwright/issues/5593
async function openRequestInFullPage(path: string, urlParams: URLSearchParams) {
return chrome.tabs.create({
url: chrome.runtime.getURL(`index.html#${path}?${urlParams.toString()}`),
});
}
import {
listenForOriginTabClose,
listenForPopupClose,
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from '@background/messaging/messaging-utils';
export function isLegacyMessage(message: any): message is LegacyMessageFromContentScript {
// Now that we use a RPC communication style, we can infer
@@ -33,58 +21,6 @@ export function isLegacyMessage(message: any): message is LegacyMessageFromConte
return !hasIdProp;
}
function getTabIdFromPort(port: chrome.runtime.Port) {
return port.sender?.tab?.id;
}
function getOriginFromPort(port: chrome.runtime.Port) {
if (port.sender?.url) return new URL(port.sender.url).origin;
return port.sender?.origin;
}
type OtherParams = [string, string][];
function makeSearchParamsWithDefaults(port: chrome.runtime.Port, otherParams: OtherParams = []) {
const urlParams = new URLSearchParams();
// All actions must have a corresponding `origin` and `tabId`
const origin = getOriginFromPort(port);
const tabId = getTabIdFromPort(port);
urlParams.set('origin', origin ?? '');
urlParams.set('tabId', tabId?.toString() ?? '');
otherParams.forEach(([key, value]) => urlParams.set(key, value));
return { urlParams, origin, tabId };
}
interface ListenForPopupCloseArgs {
// ID that comes from newly created window
id?: number;
// TabID from requesting tab, to which request should be returned
tabId?: number;
response: LegacyMessageToContentScript;
}
function listenForPopupClose({ id, tabId, response }: ListenForPopupCloseArgs) {
chrome.windows.onRemoved.addListener(winId => {
if (winId !== id || !tabId) return;
const responseMessage = response;
chrome.tabs.sendMessage(tabId, responseMessage);
});
}
interface ListenForOriginTabCloseArgs {
tabId?: number;
}
function listenForOriginTabClose({ tabId }: ListenForOriginTabCloseArgs) {
chrome.tabs.onRemoved.addListener(closedTabId => {
if (tabId !== closedTabId) return;
sendMessage({ method: InternalMethods.OriginatingTabClosed, payload: { tabId } });
});
}
async function triggerRequestWindowOpen(path: RouteUrls, urlParams: URLSearchParams) {
if (IS_TEST_ENV) return openRequestInFullPage(path, urlParams);
return popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` });
}
function getNetworkParamsFromPayload(payload: string): [string, string][] {
const { network } = getPayloadFromToken(payload);
if (!network) return [];
@@ -103,11 +39,10 @@ export async function handleLegacyExternalMethodFormat(
switch (message.method) {
case ExternalMethods.authenticationRequest: {
const otherParams: OtherParams = [
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [
['authRequest', payload],
['flow', ExternalMethods.authenticationRequest],
];
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, otherParams);
]);
const { id } = await triggerRequestWindowOpen(RouteUrls.ChooseAccount, urlParams);
listenForPopupClose({
@@ -120,12 +55,11 @@ export async function handleLegacyExternalMethodFormat(
}
case ExternalMethods.transactionRequest: {
const otherParams: OtherParams = [
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [
['request', payload],
['flow', ExternalMethods.transactionRequest],
...getNetworkParamsFromPayload(payload),
];
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, otherParams);
]);
const { id } = await triggerRequestWindowOpen(RouteUrls.TransactionRequest, urlParams);
listenForPopupClose({
@@ -138,13 +72,12 @@ export async function handleLegacyExternalMethodFormat(
}
case ExternalMethods.signatureRequest: {
const otherParams: OtherParams = [
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [
['request', payload],
['messageType', 'utf8'],
['flow', ExternalMethods.signatureRequest],
...getNetworkParamsFromPayload(payload),
];
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, otherParams);
]);
const { id } = await triggerRequestWindowOpen(RouteUrls.SignatureRequest, urlParams);
listenForPopupClose({
@@ -157,13 +90,12 @@ export async function handleLegacyExternalMethodFormat(
}
case ExternalMethods.structuredDataSignatureRequest: {
const otherParams: OtherParams = [
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [
['request', payload],
['messageType', 'structured'],
['flow', ExternalMethods.structuredDataSignatureRequest],
...getNetworkParamsFromPayload(payload),
];
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, otherParams);
]);
const { id } = await triggerRequestWindowOpen(RouteUrls.SignatureRequest, urlParams);
listenForPopupClose({

View File

@@ -5,7 +5,7 @@ import { logger } from '@shared/logger';
import { InternalMethods } from '@shared/message-types';
import { BackgroundMessages } from '@shared/messages';
import { backupWalletSaltForGaia } from './backup-old-wallet-salt';
import { backupWalletSaltForGaia } from '../backup-old-wallet-salt';
function validateMessagesAreFromExtension(sender: chrome.runtime.MessageSender) {
// Only respond to internal messages from our UI, not content scripts in other applications

View File

@@ -0,0 +1,71 @@
import { InternalMethods } from '@shared/message-types';
import { sendMessage } from '@shared/messages';
import { RouteUrls } from '@shared/route-urls';
import { popupCenter } from '@background/popup-center';
function getTabIdFromPort(port: chrome.runtime.Port) {
return port.sender?.tab?.id;
}
function getOriginFromPort(port: chrome.runtime.Port) {
if (port.sender?.url) return new URL(port.sender.url).origin;
return port.sender?.origin;
}
//
// Playwright does not currently support Chrome extension popup testing:
// https://github.com/microsoft/playwright/issues/5593
async function openRequestInFullPage(path: string, urlParams: URLSearchParams) {
return chrome.tabs.create({
url: chrome.runtime.getURL(`index.html#${path}?${urlParams.toString()}`),
});
}
interface ListenForPopupCloseArgs {
// ID that comes from newly created window
id?: number;
// TabID from requesting tab, to which request should be returned
tabId?: number;
response: any;
}
export function listenForPopupClose({ id, tabId, response }: ListenForPopupCloseArgs) {
chrome.windows.onRemoved.addListener(winId => {
if (winId !== id || !tabId) return;
const responseMessage = response;
chrome.tabs.sendMessage(tabId, responseMessage);
});
}
interface ListenForOriginTabCloseArgs {
tabId?: number;
}
export function listenForOriginTabClose({ tabId }: ListenForOriginTabCloseArgs) {
chrome.tabs.onRemoved.addListener(closedTabId => {
if (tabId !== closedTabId) return;
sendMessage({ method: InternalMethods.OriginatingTabClosed, payload: { tabId } });
});
}
type OtherParams = [string, string][];
export function makeSearchParamsWithDefaults(
port: chrome.runtime.Port,
otherParams: OtherParams = []
) {
const urlParams = new URLSearchParams();
// All actions must have a corresponding `origin` and `tabId`
const origin = getOriginFromPort(port);
const tabId = getTabIdFromPort(port);
urlParams.set('origin', origin ?? '');
urlParams.set('tabId', tabId?.toString() ?? '');
otherParams.forEach(([key, value]) => urlParams.set(key, value));
return { urlParams, origin, tabId };
}
const IS_TEST_ENV = process.env.TEST_ENV === 'true';
export async function triggerRequestWindowOpen(path: RouteUrls, urlParams: URLSearchParams) {
if (IS_TEST_ENV) return openRequestInFullPage(path, urlParams);
return popupCenter({ url: `/popup-center.html#${path}?${urlParams.toString()}` });
}

View File

@@ -0,0 +1,19 @@
import { RouteUrls } from '@shared/route-urls';
import { WalletRequests } from '@shared/rpc/rpc-methods';
import {
listenForPopupClose,
makeSearchParamsWithDefaults,
triggerRequestWindowOpen,
} from './messaging-utils';
export async function rpcMessageHander(message: WalletRequests, port: chrome.runtime.Port) {
switch (message.method) {
case 'getAddresses': {
const { urlParams, tabId } = makeSearchParamsWithDefaults(port, [['requestId', message.id]]);
const { id } = await triggerRequestWindowOpen(RouteUrls.RequestTapootAddress, urlParams);
listenForPopupClose({ tabId, id, response: { id: message.id, result: null } });
break;
}
}
}

View File

@@ -20,28 +20,6 @@ import {
MESSAGE_SOURCE,
} from '@shared/message-types';
import { RouteUrls } from '@shared/route-urls';
import { getEventSourceWindow } from '@shared/utils/get-event-source-window';
// Legacy messaging to work with older versions of Connect
window.addEventListener('message', event => {
const { data } = event;
if (data.source === 'blockstack-app') {
const { method } = data;
if (method === 'getURL') {
const url = chrome.runtime.getURL('index.html');
const source = getEventSourceWindow(event);
source?.postMessage(
{
url,
method: 'getURLResponse',
source: 'blockstack-extension',
},
event.origin
);
return;
}
}
});
// Connection to background script - fires onConnect event in background script
// and establishes two-way communication
@@ -54,8 +32,7 @@ function sendMessageToBackground(message: LegacyMessageFromContentScript) {
// Receives message from background script to execute in browser
chrome.runtime.onMessage.addListener((message: LegacyMessageToContentScript) => {
if (message.source === MESSAGE_SOURCE) {
// Forward to web app (browser)
if (message.source === MESSAGE_SOURCE || (message as any).jsonrpc === '2.0') {
window.postMessage(message, window.location.origin);
}
});
@@ -66,7 +43,6 @@ interface ForwardDomEventToBackgroundArgs {
urlParam: string;
path: RouteUrls;
}
function forwardDomEventToBackground({ payload, method }: ForwardDomEventToBackgroundArgs) {
sendMessageToBackground({
method,
@@ -75,6 +51,10 @@ function forwardDomEventToBackground({ payload, method }: ForwardDomEventToBackg
});
}
document.addEventListener(DomEventName.request, (event: any) => {
sendMessageToBackground({ source: MESSAGE_SOURCE, ...event.detail });
});
// Listen for a CustomEvent (auth request) coming from the web app
document.addEventListener(DomEventName.authenticationRequest, ((
event: AuthenticationRequestEvent

View File

@@ -1,4 +1,5 @@
import { StacksProvider } from '@stacks/connect';
import { RpcRequest } from '@btckit/types';
import { StacksProvider, getStacksProvider } from '@stacks/connect';
import { BRANCH, COMMIT_SHA } from '@shared/environment';
import {
@@ -19,20 +20,20 @@ import {
SignatureResponseMessage,
TransactionResponseMessage,
} from '@shared/message-types';
import { WalletMethodMap, WalletMethodNames, WalletResponses } from '@shared/rpc/rpc-methods';
type CallableMethods = keyof typeof ExternalMethods;
interface ExtensionResponse {
source: 'blockstack-extension';
method: CallableMethods;
[key: string]: any;
}
const callAndReceive = async (
async function callAndReceive(
methodName: CallableMethods | 'getURL',
opts: any = {}
): Promise<ExtensionResponse> => {
): Promise<ExtensionResponse> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject('Unable to get response from Blockstack extension');
@@ -57,20 +58,21 @@ const callAndReceive = async (
window.location.origin
);
});
};
}
const isValidEvent = (event: MessageEvent, method: LegacyMessageToContentScript['method']) => {
function isValidEvent(event: MessageEvent, method: LegacyMessageToContentScript['method']) {
const { data } = event;
const correctSource = data.source === MESSAGE_SOURCE;
const correctMethod = data.method === method;
return correctSource && correctMethod && !!data.payload;
};
}
const provider: StacksProvider = {
getURL: async () => {
const { url } = await callAndReceive('getURL');
return url;
},
structuredDataSignatureRequest: async signatureRequest => {
const event = new CustomEvent<SignatureRequestEventDetails>(
DomEventName.structuredDataSignatureRequest,
@@ -95,6 +97,7 @@ const provider: StacksProvider = {
window.addEventListener('message', handleMessage);
});
},
signatureRequest: async signatureRequest => {
const event = new CustomEvent<SignatureRequestEventDetails>(DomEventName.signatureRequest, {
detail: { signatureRequest },
@@ -116,6 +119,7 @@ const provider: StacksProvider = {
window.addEventListener('message', handleMessage);
});
},
authenticationRequest: async authenticationRequest => {
const event = new CustomEvent<AuthenticationRequestEventDetails>(
DomEventName.authenticationRequest,
@@ -138,6 +142,7 @@ const provider: StacksProvider = {
window.addEventListener('message', handleMessage);
});
},
transactionRequest: async transactionRequest => {
const event = new CustomEvent<TransactionRequestEventDetails>(DomEventName.transactionRequest, {
detail: { transactionRequest },
@@ -204,19 +209,58 @@ const provider: StacksProvider = {
window.addEventListener('message', handleMessage);
});
},
getProductInfo() {
return {
version: VERSION,
name: 'Hiro Wallet for Web',
name: 'Hiro Wallet',
meta: {
tag: BRANCH,
commit: COMMIT_SHA,
},
};
},
request: function (_method: string): Promise<Record<string, any>> {
throw new Error('`request` function is not implemented');
request<T extends WalletMethodNames>(
method: T,
params?: Record<string, any>
): Promise<WalletMethodMap[T]['response']> {
const id = crypto.randomUUID();
const rpcRequest: RpcRequest<T> = {
jsonrpc: '2.0',
id,
method,
params,
};
document.dispatchEvent(new CustomEvent(DomEventName.request, { detail: rpcRequest }));
return new Promise((resolve, reject) => {
function handleMessage(event: MessageEvent<WalletResponses>) {
const response = event.data;
if (!response || response.id !== id) return;
window.removeEventListener('message', handleMessage);
if ('error' in response) {
return reject(response);
}
return resolve(response);
}
window.addEventListener('message', handleMessage);
});
},
};
window.StacksProvider = provider;
(window as any).HiroWalletProvider = provider;
(window as any).btc = {
request: getStacksProvider()?.request,
listen(event: 'accountChange', callback: (arg: any) => void) {
function handler(e: MessageEvent) {
if (!e.data) return;
if ((e as any).event !== event) return;
callback((e as any).event);
}
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
},
};

View File

@@ -44,7 +44,6 @@ interface DeriveTaprootReceiveAddressIndexArgs {
index: number;
network: NetworkModes;
}
// ts-unused-exports:disable-next-line
export function deriveTaprootReceiveAddressIndex({
xpub,
index,

View File

@@ -19,7 +19,7 @@ export function deriveNativeSegWitAccountKeychain(keychain: HDKey, network: Netw
return (index: number) => keychain.derive(getNativeSegWitAccountDerivationPath(network, index));
}
export function getNativeSegWitAddressIndexDetails(keychain: HDKey, network: NetworkModes) {
export function getNativeSegWitAddressIndexFromKeychain(keychain: HDKey, network: NetworkModes) {
if (keychain.depth !== DerivationPathDepth.AddressIndex)
throw new Error('Keychain passed is not an address index');
@@ -38,5 +38,5 @@ export function deriveNativeSegWitReceiveAddressIndex({
const keychain = HDKey.fromExtendedKey(xpub);
if (!keychain) return;
const zeroAddressIndex = deriveAddressIndexZeroFromAccount(keychain);
return getNativeSegWitAddressIndexDetails(zeroAddressIndex, network);
return getNativeSegWitAddressIndexFromKeychain(zeroAddressIndex, network);
}

View File

@@ -2,6 +2,7 @@
* Inpage Script (StacksProvider) <-> Content Script
*/
export enum DomEventName {
request = 'request',
authenticationRequest = 'stacksAuthenticationRequest',
signatureRequest = 'signatureRequest',
structuredDataSignatureRequest = 'structuredDataSignatureRequest',

View File

@@ -75,4 +75,7 @@ export enum RouteUrls {
SendOrdinalInscriptionSummary = '/send/ordinal-inscription/',
SendOrdinalInscriptionSent = '/send/ordinal-inscription/sent',
SendOrdinalInscriptionError = '/send/ordinal-inscription/error',
// Request routes
RequestTapootAddress = '/taproot-address',
}

View File

@@ -0,0 +1,8 @@
import { DefineRpcMethod, RpcRequest, RpcSuccessResponse } from '@btckit/types';
// Demo method used to serve as example while we only have a single method
type TestRequest = RpcRequest<'demoMethodDeleteWhenAddingMore'>;
type TestResponse = RpcSuccessResponse<{ testResponse: string }>;
export type Test = DefineRpcMethod<TestRequest, TestResponse>;

View File

@@ -0,0 +1,28 @@
import { BtcKitMethodMap, ExtractErrorResponse, ExtractSuccessResponse } from '@btckit/types';
import { ValueOf } from '@shared/utils/type-utils';
import { Test } from './methods/test-method';
export type WalletMethodMap = BtcKitMethodMap & Test;
export type WalletRequests = ValueOf<WalletMethodMap>['request'];
export type WalletResponses = ValueOf<WalletMethodMap>['response'];
export type WalletMethodNames = keyof WalletMethodMap;
export function makeRpcSuccessResponse<T extends WalletMethodNames>(
_method: T,
response: Omit<ExtractSuccessResponse<WalletMethodMap[T]['response']>, 'jsonrpc'>
): WalletMethodMap[T]['response'] {
// typecasting as there's a error stating jsonrpc prop is already there
return { jsonrpc: '2.0', ...response } as WalletMethodMap[T]['response'];
}
// ts-unused-exports:disable-next-line
export function makeRpcErrorResponse<T extends WalletMethodNames>(
_method: T,
response: Omit<ExtractErrorResponse<WalletMethodMap[T]['response']>, 'jsonrpc'>
) {
return { jsonrpc: '2.0', ...response } as WalletMethodMap[T]['response'];
}

View File

@@ -1,8 +0,0 @@
export const getEventSourceWindow = (event: MessageEvent) => {
const isWindow =
!(event.source instanceof MessagePort) && !(event.source instanceof ServiceWorker);
if (isWindow) {
return event.source as Window;
}
return null;
};

View File

@@ -1,3 +1,5 @@
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export type ValueOf<T> = T[keyof T];

View File

@@ -1,21 +1,32 @@
import React, { useState } from 'react';
import '@btckit/types';
import { demoTokenContract } from '@common/contracts';
import { useSTXAddress } from '@common/use-stx-address';
import {
stacksTestnetNetwork as network,
stacksLocalhostNetwork,
stacksMainnetNetwork,
stacksTestnetNetwork,
} from '@common/utils';
import { useConnect } from '@stacks/connect-react';
import { StacksMainnet, StacksTestnet } from '@stacks/network';
import {
FungibleConditionCode,
NonFungibleConditionCode,
PostConditionMode,
StacksTransaction,
broadcastTransaction,
bufferCV,
bufferCVFromString,
createAssetInfo,
createNonFungiblePostCondition,
FungibleConditionCode,
intCV,
makeStandardFungiblePostCondition,
makeStandardSTXPostCondition,
noneCV,
NonFungibleConditionCode,
PostConditionMode,
someCV,
sponsorTransaction,
StacksTransaction,
standardPrincipalCV,
stringAsciiCV,
stringUtf8CV,
@@ -24,20 +35,10 @@ import {
uintCV,
} from '@stacks/transactions';
import { Box, Button, ButtonGroup, Text } from '@stacks/ui';
import BN from 'bn.js';
import React, { useState } from 'react';
import { demoTokenContract } from '@common/contracts';
import { useSTXAddress } from '@common/use-stx-address';
import {
stacksLocalhostNetwork,
stacksMainnetNetwork,
stacksTestnetNetwork as network,
stacksTestnetNetwork,
} from '@common/utils';
import { TransactionSigningSelectors } from '@tests-legacy/page-objects/transaction-signing.selectors';
import { WalletPageSelectors } from '@tests-legacy/page-objects/wallet.selectors';
import BN from 'bn.js';
import { ExplorerLink } from './explorer-link';
export const Debugger = () => {
@@ -446,6 +447,21 @@ export const Debugger = () => {
>
Request API info
</Button>
<Button
onClick={() => {
console.log('requesting');
window.StacksProvider?.request('getAddresses')
.then(resp => {
console.log({ sucesss: resp });
})
.catch(error => {
console.log({ error });
});
}}
>
RPC test
</Button>
</ButtonGroup>
</Box>
</Box>

View File

@@ -674,6 +674,11 @@
sha.js "^2.4.11"
smart-buffer "^4.1.0"
"@btckit/types@0.0.11":
version "0.0.11"
resolved "https://registry.yarnpkg.com/@btckit/types/-/types-0.0.11.tgz#dcc1f51beb621d091ad4a5f6cab4c365f1ae497e"
integrity sha512-9aqJd/Aw1PMKFN935tFh3ZajTIEVp+dwpSKoHf1d8tUc3tiu7Q2nF47OkxUvtQb+dHRHPY0HrSW+uH0B3VStAg==
"@coinbase/cbpay-js@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@coinbase/cbpay-js/-/cbpay-js-1.0.2.tgz#4975efa6b060868c0a6cda20bb6e6169f0d82b0b"