diff --git a/.dependency-cruiser.js b/.dependency-cruiser.js index ed3e2367473..345cfdee961 100644 --- a/.dependency-cruiser.js +++ b/.dependency-cruiser.js @@ -19,6 +19,21 @@ module.exports = { path: '^(domain|constants|sys|_linklist|_stream_wrap)$', }, }, + // Overriding from recommended set + { + name: 'not-to-unresolvable', + comment: + "This module depends on a module that cannot be found ('resolved to disk'). " + + "If it's an npm module: add it to your package.json. In all other cases you " + + 'likely already know what to do.', + severity: 'error', + from: {}, + to: { + // Depcruiser fails on some legitimate type imports, so allowing them there + dependencyTypesNot: ['type-only'], + couldNotResolve: true, + }, + }, { name: 'no-orphans', severity: 'error', diff --git a/.github/workflows/build-extension.yml b/.github/workflows/build-extension.yml index 181de7b276b..4dc89dedcc9 100644 --- a/.github/workflows/build-extension.yml +++ b/.github/workflows/build-extension.yml @@ -1,25 +1,16 @@ name: PR Extensions builds on: [pull_request, workflow_dispatch] -env: - COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} - MOONPAY_API_KEY: ${{ secrets.MOONPAY_API_KEY }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY_STAGING }} - TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} - WALLET_ENVIRONMENT: feature - jobs: pre-run: runs-on: ubuntu-latest steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@ad6cb1b847ffb509a69b745b6ee2f1d14dfe14b8 + uses: styfle/cancel-workflow-action@main with: access_token: ${{ github.token }} - update-pull-request-body: - name: Add links to built extensions + set-in-progress-message: if: github.repository == 'leather-wallet/extension' && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest needs: @@ -27,68 +18,54 @@ jobs: steps: - uses: kyranjamie/pull-request-fixed-header@v1.0.1 with: - header: '> Try out this version of Leather — [download extension builds](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}).' + header: '> _Building Leather…_' GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build_chrome_extension: - name: Build debug Chrome extension + build: + name: build-${{ matrix.target }}-extension runs-on: ubuntu-latest needs: - pre-run + strategy: + matrix: + target: [chromium, firefox] + env: + WALLET_ENVIRONMENT: feature + TARGET_BROWSER: ${{ matrix.target }} + COINBASE_APP_ID: ${{ secrets.COINBASE_APP_ID }} + MOONPAY_API_KEY: ${{ secrets.MOONPAY_API_KEY }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN }} + SEGMENT_WRITE_KEY: ${{ secrets.SEGMENT_WRITE_KEY_STAGING }} + TRANSAK_API_KEY: ${{ secrets.TRANSAK_API_KEY }} + PR_NUMBER: ${{ github.event.number }} + COMMIT_SHA: ${{ github.event.pull_request.head.sha }} steps: - uses: actions/checkout@v4 - - - uses: actions/cache@v3 - id: cache-node-modules with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} + ref: ${{ github.event.pull_request.head.sha }} - uses: ./.github/actions/provision + - name: Add SHORT_SHA env property with commit short sha + run: echo "SHORT_SHA=`echo ${GITHUB_SHA} | cut -c1-8`" >> $GITHUB_ENV + - name: Build project run: yarn build - - name: Build extension - run: sh build-ext.sh - - uses: actions/upload-artifact@v3 - name: Upload Chrome Extension Zip + name: Upload ${{ matrix.target }} Extension Zip with: - name: stacks-wallet-chromium - path: leather-chromium.zip + name: leather-${{ matrix.target }}-${{ env.SHORT_SHA }} + path: dist - build_firefox: - name: Build debug Firefox extension + set-download-link: + if: github.repository == 'leather-wallet/extension' && github.actor != 'dependabot[bot]' runs-on: ubuntu-latest needs: - pre-run - env: - TARGET_BROWSER: firefox + - build steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.ref }} - - - uses: actions/cache@v3 - id: cache-node-modules - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**/package.json') }} - - - uses: ./.github/actions/provision - - - name: Build project - run: yarn build - - - name: Build extension - run: sh build-ext.sh - - - name: Rename file - run: mv leather-chromium.zip leather-firefox.zip - - - uses: actions/upload-artifact@v3 + - uses: kyranjamie/pull-request-fixed-header@v1.0.1 with: - name: stacks-wallet-firefox - path: leather-firefox.zip + header: '> Try out this version of Leather — [download extension builds](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}).' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-extensions.yml b/.github/workflows/publish-extensions.yml index 0fff9ddcbfe..838f38716e5 100644 --- a/.github/workflows/publish-extensions.yml +++ b/.github/workflows/publish-extensions.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@ad6cb1b847ffb509a69b745b6ee2f1d14dfe14b8 + uses: styfle/cancel-workflow-action@main with: access_token: ${{ github.token }} diff --git a/package.json b/package.json index 01922e72af3..956cbd08cf5 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "version": "6.9.2", "author": "Hiro Systems PBC", "scripts": { - "dev": "cross-env WALLET_ENVIRONMENT=development concurrently --raw \"node webpack/dev-server.js\" \"redux-devtools --hostname=localhost --port=8000\"", + "dev": "concurrently --raw \"node webpack/dev-server.js\" \"redux-devtools --hostname=localhost --port=8000\"", "dev:test-app": "webpack serve --config test-app/webpack/webpack.config.dev.js", - "build": "cross-env WALLET_ENVIRONMENT=production webpack --config webpack/webpack.config.prod.js", - "build:analyze": "cross-env ANALYZE=true WALLET_ENVIRONMENT=production webpack --config webpack/webpack.config.prod.js", + "build": "webpack --config webpack/webpack.config.prod.js", + "build:analyze": "cross-env ANALYZE=true webpack --config webpack/webpack.config.prod.js", "build:dev": "cross-env WALLET_ENVIRONMENT=development webpack --config webpack/webpack.config.dev.js", "build:ext:test": "cross-env WALLET_ENVIRONMENT=testing webpack --config webpack/webpack.config.prod.js", "build:ext:test:watch": "cross-env WALLET_ENVIRONMENT=testing webpack --config webpack/webpack.config.prod.js --watch", @@ -128,6 +128,7 @@ "@ledgerhq/hw-transport-webusb": "6.27.19", "@noble/hashes": "1.3.2", "@noble/secp256k1": "2.0.0", + "@octokit/types": "12.0.0", "@radix-ui/colors": "2.1.0", "@radix-ui/react-accessible-icon": "1.0.3", "@radix-ui/react-switch": "1.0.3", @@ -289,7 +290,7 @@ "cross-env": "7.0.3", "crypto-browserify": "3.12.0", "deepmerge": "4.3.1", - "dependency-cruiser": "14.1.0", + "dependency-cruiser": "14.1.1", "dotenv-webpack": "8.0.1", "esbuild": "0.19.4", "esbuild-loader": "4.0.2", diff --git a/scripts/generate-manifest.js b/scripts/generate-manifest.js index ca75b46fe8d..80bca9acdb0 100644 --- a/scripts/generate-manifest.js +++ b/scripts/generate-manifest.js @@ -3,9 +3,11 @@ */ const deepMerge = require('deepmerge'); -const IS_DEV = process.env.WALLET_ENVIRONMENT === 'development'; +// Manifest can only be prod or dev +const WALLET_ENVIRONMENT = + process.env.WALLET_ENVIRONMENT === 'production' ? 'production' : 'development'; -const WALLET_ENVIRONMENT = process.env.WALLET_ENVIRONMENT ?? 'development'; +const IS_DEV = WALLET_ENVIRONMENT === 'development'; const PREVIEW_RELEASE = process.env.PREVIEW_RELEASE; @@ -20,9 +22,6 @@ function generateImageAssetUrlsWithSuffix(suffix = '') { } const environmentIcons = { - testing: { - icons: generateImageAssetUrlsWithSuffix(PREVIEW_RELEASE ? '-preview' : ''), - }, development: { icons: generateImageAssetUrlsWithSuffix('-dev'), }, @@ -32,14 +31,12 @@ const environmentIcons = { }; const contentSecurityPolicyEnvironment = { - testing: `default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' data: https:; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';`, development: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; frame-src 'none'; frame-ancestors 'none';", production: `default-src 'none'; connect-src *; style-src 'unsafe-inline'; img-src 'self' data: https:; script-src 'self' 'wasm-unsafe-eval'; object-src 'none'; frame-src 'none'; frame-ancestors 'none';`, }; const defaultIconEnvironment = { - testing: 'assets/connect-logo/Stacks128w.png', development: 'assets/connect-logo/Stacks128w-dev.png', production: 'assets/connect-logo/Stacks128w.png', }; diff --git a/src/app/components/app-version.tsx b/src/app/components/app-version.tsx new file mode 100644 index 00000000000..21d416c9bb9 --- /dev/null +++ b/src/app/components/app-version.tsx @@ -0,0 +1,63 @@ +import { useMemo } from 'react'; +import { forwardRef } from 'react'; + +import { HTMLStyledProps, styled } from 'leather-styles/jsx'; + +import { BRANCH_NAME, COMMIT_SHA } from '@shared/environment'; + +import { openInNewTab } from '@app/common/utils/open-in-new-tab'; +import { useIsLatestPullRequestBuild } from '@app/query/common/outdated-pr/outdated-pr.query'; + +import { Tooltip } from './tooltip'; + +interface AppVersionLabelProps extends HTMLStyledProps<'span'> { + isLatestVersion: boolean; +} +const AppVersionLabel = forwardRef( + ({ children, isLatestVersion, ...props }: AppVersionLabelProps, ref) => ( + + {children} + + ) +); + +export function AppVersion() { + const { pullRequestLink, isLatestBuild } = useIsLatestPullRequestBuild(); + + const version = useMemo(() => { + switch (process.env.WALLET_ENVIRONMENT) { + case 'development': + return `dev@${BRANCH_NAME}`; + case 'feature': + return `${BRANCH_NAME}#${COMMIT_SHA?.slice(0, 8)}`; + default: + return `v${VERSION}`; + } + }, []); + + if (!isLatestBuild && process.env.WALLET_ENVIRONMENT === 'feature') { + return ( + + openInNewTab(pullRequestLink ?? '')} + > + {version} + + + ); + } + + return {version}; +} diff --git a/src/app/components/fees-row/components/custom-fee-field.tsx b/src/app/components/fees-row/components/custom-fee-field.tsx index 2572d8c95b8..a5a8f4a7d62 100644 --- a/src/app/components/fees-row/components/custom-fee-field.tsx +++ b/src/app/components/fees-row/components/custom-fee-field.tsx @@ -15,9 +15,11 @@ interface CustomFeeFieldProps extends StackProps { feeCurrencySymbol: CryptoCurrencies; lowFeeEstimate: StacksFeeEstimate; setFieldWarning(value: string): void; + disableFeeSelection?: boolean; } export function CustomFeeField(props: CustomFeeFieldProps) { - const { feeCurrencySymbol, lowFeeEstimate, setFieldWarning, ...rest } = props; + const { feeCurrencySymbol, lowFeeEstimate, setFieldWarning, disableFeeSelection, ...rest } = + props; const [field, meta, helpers] = useField('fee'); const checkFieldWarning = useCallback( @@ -52,6 +54,7 @@ export function CustomFeeField(props: CustomFeeFieldProps) { display="block" height="32px" name="fee" + isDisabled={disableFeeSelection} onChange={(evt: FormEvent) => { helpers.setValue(evt.currentTarget.value); // Separating warning check from field validations diff --git a/src/app/components/fees-row/components/fee-estimate-item.tsx b/src/app/components/fees-row/components/fee-estimate-item.tsx index ec7b17a8afa..f5f60e18eae 100644 --- a/src/app/components/fees-row/components/fee-estimate-item.tsx +++ b/src/app/components/fees-row/components/fee-estimate-item.tsx @@ -11,9 +11,10 @@ interface FeeEstimateItemProps { isVisible?: boolean; onSelectItem(index: number): void; selectedItem: number; + disableFeeSelection?: boolean; } export function FeeEstimateItem(props: FeeEstimateItemProps) { - const { index, isVisible, onSelectItem, selectedItem } = props; + const { index, isVisible, onSelectItem, selectedItem, disableFeeSelection } = props; const selectedIcon = useMemo(() => { const isSelected = index === selectedItem; @@ -29,13 +30,13 @@ export function FeeEstimateItem(props: FeeEstimateItemProps) { isInline mb="0px !important" minWidth="100px" - onClick={() => onSelectItem(index)} + onClick={() => !disableFeeSelection && onSelectItem(index)} p="tight" > {labels[index]} - {isVisible ? selectedIcon : } + {!disableFeeSelection && (isVisible ? selectedIcon : )} ); } diff --git a/src/app/components/fees-row/components/fee-estimate-select.layout.tsx b/src/app/components/fees-row/components/fee-estimate-select.layout.tsx index 0e2988812b6..d0c5fa78e0f 100644 --- a/src/app/components/fees-row/components/fee-estimate-select.layout.tsx +++ b/src/app/components/fees-row/components/fee-estimate-select.layout.tsx @@ -20,17 +20,19 @@ interface FeeEstimateSelectLayoutProps { isVisible: boolean; onSetIsSelectVisible(value: boolean): void; selectedItem: number; + disableFeeSelection?: boolean; } export function FeeEstimateSelectLayout(props: FeeEstimateSelectLayoutProps) { - const { children, isVisible, onSetIsSelectVisible, selectedItem } = props; + const { children, isVisible, onSetIsSelectVisible, selectedItem, disableFeeSelection } = props; const ref = useRef(null); useOnClickOutside(ref, () => onSetIsSelectVisible(false)); return ( <> - + onSetIsSelectVisible(true)} selectedItem={FeeTypes.Middle} diff --git a/src/app/components/fees-row/components/fee-estimate-select.tsx b/src/app/components/fees-row/components/fee-estimate-select.tsx index 5ff6ada1414..f2503471209 100644 --- a/src/app/components/fees-row/components/fee-estimate-select.tsx +++ b/src/app/components/fees-row/components/fee-estimate-select.tsx @@ -11,13 +11,22 @@ interface FeeEstimateSelectProps { onSetIsSelectVisible(value: boolean): void; selectedItem: number; allowCustom: boolean; + disableFeeSelection?: boolean; } export function FeeEstimateSelect(props: FeeEstimateSelectProps) { - const { isVisible, estimate, onSelectItem, onSetIsSelectVisible, selectedItem, allowCustom } = - props; + const { + isVisible, + estimate, + onSelectItem, + onSetIsSelectVisible, + selectedItem, + allowCustom, + disableFeeSelection, + } = props; return ( { - if (hasFeeEstimates && !feeField.value && !isCustom) { + if (props.defaultFeeValue) { + feeHelper.setValue( + convertAmountToBaseUnit( + new BigNumber(Number(props.defaultFeeValue)), + STX_DECIMALS + ).toString() + ); + feeTypeHelper.setValue(FeeTypes[FeeTypes.Custom]); + } + }, [feeHelper, props.defaultFeeValue, feeTypeHelper]); + + useEffect(() => { + if (!props.defaultFeeValue && hasFeeEstimates && !feeField.value && !isCustom) { feeHelper.setValue(convertAmountToBaseUnit(fees.estimates[FeeTypes.Middle].fee).toString()); feeTypeHelper.setValue(FeeTypes[FeeTypes.Middle]); } @@ -54,6 +69,7 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element { feeHelper.setValue(0); } }, [ + props.defaultFeeValue, feeField.value, feeHelper, feeTypeHelper, @@ -66,13 +82,18 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element { const handleSelectFeeEstimateOrCustomField = useCallback( (index: number) => { feeTypeHelper.setValue(FeeTypes[index]); - if (index === FeeTypes.Custom) feeHelper.setValue(''); + if (index === FeeTypes.Custom) + feeHelper.setValue( + props.defaultFeeValue + ? convertAmountToBaseUnit(new BigNumber(Number(props.defaultFeeValue)), STX_DECIMALS) + : '' + ); else fees && feeHelper.setValue(convertAmountToBaseUnit(fees.estimates[index].fee).toString()); setFieldWarning(''); setIsSelectVisible(false); }, - [feeTypeHelper, feeHelper, fees] + [feeTypeHelper, feeHelper, fees, props.defaultFeeValue] ); if (!hasFeeEstimates) return ; @@ -83,6 +104,7 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element { feeField={ isCustom ? ( setFieldWarning(value)} @@ -101,6 +123,7 @@ export function FeesRow(props: FeeRowProps): React.JSX.Element { isSponsored={isSponsored} selectInput={ = memo(props => { +export function Header(props: HeaderProps) { const { actionButton, hideActions, onClose, title, ...rest } = props; const { isShowingSettings, setIsShowingSettings } = useDrawers(); const { pathname } = useLocation(); @@ -41,20 +41,6 @@ export const Header: React.FC = memo(props => { ); }, [pathname]); - const version = useMemo(() => { - switch (process.env.WALLET_ENVIRONMENT) { - case 'production': - case 'preview': - return `v${VERSION}`; - case 'feature': - return `${BRANCH_NAME}#${COMMIT_SHA?.slice(0, 8)}`; - case 'development': - return 'dev'; - default: - return null; - } - }, []); - return ( = memo(props => { isClickable={leatherLogoIsClickable} onClick={leatherLogoIsClickable ? () => navigate(RouteUrls.Home) : undefined} /> - - {version} - + ) : ( @@ -128,4 +104,4 @@ export const Header: React.FC = memo(props => { ); -}); +} diff --git a/src/app/components/nonce-setter.tsx b/src/app/components/nonce-setter.tsx index a34e6355d7f..6fd0e5acb41 100644 --- a/src/app/components/nonce-setter.tsx +++ b/src/app/components/nonce-setter.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect } from 'react'; +import { useEffect } from 'react'; import { useFormikContext } from 'formik'; @@ -6,10 +6,7 @@ import { StacksSendFormValues, StacksTransactionFormValues } from '@shared/model import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; -interface NonceSetterProps { - children: ReactNode; -} -export function NonceSetter({ children }: NonceSetterProps) { +export function NonceSetter() { const { setFieldValue, touched, values } = useFormikContext< StacksSendFormValues | StacksTransactionFormValues >(); @@ -21,5 +18,5 @@ export function NonceSetter({ children }: NonceSetterProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [nextNonce?.nonce]); - return <>{children}; + return <>; } diff --git a/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx b/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx index 845934bcd39..938c2f98910 100644 --- a/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx +++ b/src/app/features/edit-nonce-drawer/edit-nonce-drawer.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { useFormikContext } from 'formik'; import { Stack, styled } from 'leather-styles/jsx'; @@ -33,10 +33,11 @@ export function EditNonceDrawer() { const [loadedNextNonce, setLoadedNextNonce] = useState(); const navigate = useNavigate(); + const { search } = useLocation(); useOnMount(() => setLoadedNextNonce(values.nonce)); - const onGoBack = useCallback(() => navigate('..'), [navigate]); + const onGoBack = useCallback(() => navigate('..' + search), [navigate, search]); const onBlur = useCallback(() => validateField('nonce'), [validateField]); diff --git a/src/app/pages/transaction-request/components/attachment-row.tsx b/src/app/features/stacks-transaction-request/attachment-row.tsx similarity index 100% rename from src/app/pages/transaction-request/components/attachment-row.tsx rename to src/app/features/stacks-transaction-request/attachment-row.tsx diff --git a/src/app/pages/transaction-request/components/contract-call-details/contract-call-details.tsx b/src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx similarity index 90% rename from src/app/pages/transaction-request/components/contract-call-details/contract-call-details.tsx rename to src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx index c44e5346191..b71e4d8b44b 100644 --- a/src/app/pages/transaction-request/components/contract-call-details/contract-call-details.tsx +++ b/src/app/features/stacks-transaction-request/contract-call-details/contract-call-details.tsx @@ -6,8 +6,8 @@ import { useExplorerLink } from '@app/common/hooks/use-explorer-link'; import { formatContractId } from '@app/common/utils'; import { Divider } from '@app/components/layout/divider'; import { Title } from '@app/components/typography'; -import { AttachmentRow } from '@app/pages/transaction-request/components/attachment-row'; -import { ContractPreviewLayout } from '@app/pages/transaction-request/components/contract-preview'; +import { AttachmentRow } from '@app/features/stacks-transaction-request/attachment-row'; +import { ContractPreviewLayout } from '@app/features/stacks-transaction-request/contract-preview'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; import { FunctionArgumentsList } from './function-arguments-list'; diff --git a/src/app/pages/transaction-request/components/contract-call-details/function-argument-item.tsx b/src/app/features/stacks-transaction-request/contract-call-details/function-argument-item.tsx similarity index 90% rename from src/app/pages/transaction-request/components/contract-call-details/function-argument-item.tsx rename to src/app/features/stacks-transaction-request/contract-call-details/function-argument-item.tsx index 203e95fb395..9c737d2d958 100644 --- a/src/app/pages/transaction-request/components/contract-call-details/function-argument-item.tsx +++ b/src/app/features/stacks-transaction-request/contract-call-details/function-argument-item.tsx @@ -1,6 +1,6 @@ import { cvToString, deserializeCV, getCVTypeString } from '@stacks/transactions'; -import { Row } from '@app/pages/transaction-request/components/row'; +import { Row } from '@app/features/stacks-transaction-request/row'; import { useContractFunction } from '@app/query/stacks/contract/contract.hooks'; interface FunctionArgumentProps { diff --git a/src/app/pages/transaction-request/components/contract-call-details/function-arguments-list.tsx b/src/app/features/stacks-transaction-request/contract-call-details/function-arguments-list.tsx similarity index 100% rename from src/app/pages/transaction-request/components/contract-call-details/function-arguments-list.tsx rename to src/app/features/stacks-transaction-request/contract-call-details/function-arguments-list.tsx diff --git a/src/app/pages/transaction-request/components/contract-deploy-details/contract-deploy-details.tsx b/src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx similarity index 92% rename from src/app/pages/transaction-request/components/contract-deploy-details/contract-deploy-details.tsx rename to src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx index 729d00d1695..f05bbeeabb4 100644 --- a/src/app/pages/transaction-request/components/contract-deploy-details/contract-deploy-details.tsx +++ b/src/app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details.tsx @@ -5,9 +5,9 @@ import { BoxProps, CodeBlock, Stack, color } from '@stacks/ui'; import { Prism } from '@app/common/clarity-prism'; import { Divider } from '@app/components/layout/divider'; import { Caption, Title } from '@app/components/typography'; -import { AttachmentRow } from '@app/pages/transaction-request/components/attachment-row'; -import { ContractPreviewLayout } from '@app/pages/transaction-request/components/contract-preview'; -import { Row } from '@app/pages/transaction-request/components/row'; +import { AttachmentRow } from '@app/features/stacks-transaction-request/attachment-row'; +import { ContractPreviewLayout } from '@app/features/stacks-transaction-request/contract-preview'; +import { Row } from '@app/features/stacks-transaction-request/row'; import { useCurrentAccountStxAddressState, useCurrentStacksAccount, diff --git a/src/app/pages/transaction-request/components/contract-preview.tsx b/src/app/features/stacks-transaction-request/contract-preview.tsx similarity index 100% rename from src/app/pages/transaction-request/components/contract-preview.tsx rename to src/app/features/stacks-transaction-request/contract-preview.tsx diff --git a/src/app/pages/transaction-request/components/fee-form.tsx b/src/app/features/stacks-transaction-request/fee-form.tsx similarity index 72% rename from src/app/pages/transaction-request/components/fee-form.tsx rename to src/app/features/stacks-transaction-request/fee-form.tsx index a32a3c15c0b..d97c22fe1ef 100644 --- a/src/app/pages/transaction-request/components/fee-form.tsx +++ b/src/app/features/stacks-transaction-request/fee-form.tsx @@ -10,9 +10,11 @@ import { useUnsignedPrepareTransactionDetails } from '@app/store/transactions/tr interface FeeFormProps { fees?: Fees; + disableFeeSelection?: boolean; + defaultFeeValue?: number; } -export function FeeForm({ fees }: FeeFormProps) { +export function FeeForm({ fees, disableFeeSelection, defaultFeeValue }: FeeFormProps) { const { values } = useFormikContext(); const transaction = useUnsignedPrepareTransactionDetails(values); @@ -21,7 +23,12 @@ export function FeeForm({ fees }: FeeFormProps) { return ( <> {fees?.estimates.length ? ( - + ) : ( )} diff --git a/src/app/pages/transaction-request/hooks/use-page-title.ts b/src/app/features/stacks-transaction-request/hooks/use-page-title.ts similarity index 100% rename from src/app/pages/transaction-request/hooks/use-page-title.ts rename to src/app/features/stacks-transaction-request/hooks/use-page-title.ts diff --git a/src/app/pages/transaction-request/hooks/use-transaction-error.ts b/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts similarity index 88% rename from src/app/pages/transaction-request/hooks/use-transaction-error.ts rename to src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts index 325c8d58e29..fc980743c07 100644 --- a/src/app/pages/transaction-request/hooks/use-transaction-error.ts +++ b/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts @@ -4,14 +4,19 @@ import { ContractCallPayload, TransactionTypes } from '@stacks/connect'; import BigNumber from 'bignumber.js'; import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; +import { initialSearchParams } from '@app/common/initial-search-params'; import { microStxToStx } from '@app/common/money/unit-conversion'; import { validateStacksAddress } from '@app/common/stacks-utils'; -import { TransactionErrorReason } from '@app/pages/transaction-request/components/transaction-error/transaction-error'; +import { TransactionErrorReason } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; import { useCurrentStacksAccountAnchoredBalances } from '@app/query/stacks/balance/stx-balance.hooks'; import { useContractInterface } from '@app/query/stacks/contract/contract.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; +function getIsMultisig() { + return initialSearchParams.get('isMultisig') === 'true'; +} + export function useTransactionError() { const transactionRequest = useTransactionRequestState(); const contractInterface = useContractInterface(transactionRequest as ContractCallPayload); @@ -33,7 +38,7 @@ export function useTransactionError() { if ((contractInterface as any)?.isError) return TransactionErrorReason.NoContract; } - if (balances) { + if (balances && !getIsMultisig()) { const zeroBalance = balances?.stx.unlockedStx.amount.toNumber() === 0; if (transactionRequest.txType === TransactionTypes.STXTransfer) { diff --git a/src/app/pages/transaction-request/components/minimal-error-message.tsx b/src/app/features/stacks-transaction-request/minimal-error-message.tsx similarity index 88% rename from src/app/pages/transaction-request/components/minimal-error-message.tsx rename to src/app/features/stacks-transaction-request/minimal-error-message.tsx index 2138f8a1a47..eea14468c39 100644 --- a/src/app/pages/transaction-request/components/minimal-error-message.tsx +++ b/src/app/features/stacks-transaction-request/minimal-error-message.tsx @@ -5,8 +5,8 @@ import { TransactionRequestSelectors } from '@tests/selectors/requests.selectors import { styled } from 'leather-styles/jsx'; import { ErrorIcon } from '@app/components/icons/error-icon'; -import { TransactionErrorReason } from '@app/pages/transaction-request/components/transaction-error/transaction-error'; -import { useTransactionError } from '@app/pages/transaction-request/hooks/use-transaction-error'; +import { useTransactionError } from '@app/features/stacks-transaction-request/hooks/use-transaction-error'; +import { TransactionErrorReason } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; function MinimalErrorMessageSuspense(props: StackProps) { const error = useTransactionError(); diff --git a/src/app/pages/transaction-request/components/page-top.tsx b/src/app/features/stacks-transaction-request/page-top.tsx similarity index 95% rename from src/app/pages/transaction-request/components/page-top.tsx rename to src/app/features/stacks-transaction-request/page-top.tsx index 336e2cec4b7..d98d45258bf 100644 --- a/src/app/pages/transaction-request/components/page-top.tsx +++ b/src/app/features/stacks-transaction-request/page-top.tsx @@ -8,7 +8,7 @@ import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-s import { addPortSuffix, getUrlHostname } from '@app/common/utils'; import { Favicon } from '@app/components/favicon'; import { Flag } from '@app/components/layout/flag'; -import { usePageTitle } from '@app/pages/transaction-request/hooks/use-page-title'; +import { usePageTitle } from '@app/features/stacks-transaction-request/hooks/use-page-title'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; diff --git a/src/app/pages/transaction-request/components/post-condition-mode-warning.tsx b/src/app/features/stacks-transaction-request/post-condition-mode-warning.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-condition-mode-warning.tsx rename to src/app/features/stacks-transaction-request/post-condition-mode-warning.tsx diff --git a/src/app/pages/transaction-request/components/post-conditions/fungible-post-condition-item.tsx b/src/app/features/stacks-transaction-request/post-conditions/fungible-post-condition-item.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-conditions/fungible-post-condition-item.tsx rename to src/app/features/stacks-transaction-request/post-conditions/fungible-post-condition-item.tsx diff --git a/src/app/pages/transaction-request/components/post-conditions/no-post-conditions.tsx b/src/app/features/stacks-transaction-request/post-conditions/no-post-conditions.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-conditions/no-post-conditions.tsx rename to src/app/features/stacks-transaction-request/post-conditions/no-post-conditions.tsx diff --git a/src/app/pages/transaction-request/components/post-conditions/post-condition-item.tsx b/src/app/features/stacks-transaction-request/post-conditions/post-condition-item.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-conditions/post-condition-item.tsx rename to src/app/features/stacks-transaction-request/post-conditions/post-condition-item.tsx diff --git a/src/app/pages/transaction-request/components/post-conditions/post-conditions-list.tsx b/src/app/features/stacks-transaction-request/post-conditions/post-conditions-list.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-conditions/post-conditions-list.tsx rename to src/app/features/stacks-transaction-request/post-conditions/post-conditions-list.tsx diff --git a/src/app/pages/transaction-request/components/post-conditions/post-conditions.tsx b/src/app/features/stacks-transaction-request/post-conditions/post-conditions.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-conditions/post-conditions.tsx rename to src/app/features/stacks-transaction-request/post-conditions/post-conditions.tsx diff --git a/src/app/pages/transaction-request/components/post-conditions/stx-post-condition.tsx b/src/app/features/stacks-transaction-request/post-conditions/stx-post-condition.tsx similarity index 100% rename from src/app/pages/transaction-request/components/post-conditions/stx-post-condition.tsx rename to src/app/features/stacks-transaction-request/post-conditions/stx-post-condition.tsx diff --git a/src/app/pages/transaction-request/components/principal-value.tsx b/src/app/features/stacks-transaction-request/principal-value.tsx similarity index 100% rename from src/app/pages/transaction-request/components/principal-value.tsx rename to src/app/features/stacks-transaction-request/principal-value.tsx diff --git a/src/app/pages/transaction-request/components/row.tsx b/src/app/features/stacks-transaction-request/row.tsx similarity index 100% rename from src/app/pages/transaction-request/components/row.tsx rename to src/app/features/stacks-transaction-request/row.tsx diff --git a/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx b/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx new file mode 100644 index 00000000000..845a1199259 --- /dev/null +++ b/src/app/features/stacks-transaction-request/stacks-transaction-signer.tsx @@ -0,0 +1,139 @@ +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; + +import { StacksTransaction } from '@stacks/transactions'; +import { Flex } from '@stacks/ui'; +import { Formik } from 'formik'; +import * as yup from 'yup'; + +import { HIGH_FEE_WARNING_LEARN_MORE_URL_STX } from '@shared/constants'; +import { FeeTypes } from '@shared/models/fees/fees.model'; +import { StacksTransactionFormValues } from '@shared/models/form.model'; +import { RouteUrls } from '@shared/route-urls'; + +import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; +import { useOnMount } from '@app/common/hooks/use-on-mount'; +import { useRouteHeader } from '@app/common/hooks/use-route-header'; +import { stxToMicroStx } from '@app/common/money/unit-conversion'; +import { stxFeeValidator } from '@app/common/validation/forms/fee-validators'; +import { nonceValidator } from '@app/common/validation/nonce-validators'; +import { EditNonceButton } from '@app/components/edit-nonce-button'; +import { NonceSetter } from '@app/components/nonce-setter'; +import { PopupHeader } from '@app/features/current-account/popup-header'; +import { RequestingTabClosedWarningMessage } from '@app/features/errors/requesting-tab-closed-error-msg'; +import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer'; +import { ContractCallDetails } from '@app/features/stacks-transaction-request/contract-call-details/contract-call-details'; +import { ContractDeployDetails } from '@app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details'; +import { PageTop } from '@app/features/stacks-transaction-request/page-top'; +import { PostConditionModeWarning } from '@app/features/stacks-transaction-request/post-condition-mode-warning'; +import { PostConditions } from '@app/features/stacks-transaction-request/post-conditions/post-conditions'; +import { StxTransferDetails } from '@app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details'; +import { TransactionError } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; +import { useCurrentStacksAccountAnchoredBalances } from '@app/query/stacks/balance/stx-balance.hooks'; +import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks'; +import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; +import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; + +import { FeeForm } from './fee-form'; +import { MinimalErrorMessage } from './minimal-error-message'; +import { SubmitAction } from './submit-action'; + +interface StacksTransactionSignerProps { + stacksTransaction: StacksTransaction; + disableFeeSelection?: boolean; + disableNonceSelection?: boolean; + isMultisig: boolean; + + onCancel(): void; + + onSignStacksTransaction(fee: number, nonce: number): void; +} + +export function StacksTransactionSigner({ + stacksTransaction, + disableFeeSelection, + disableNonceSelection, + onSignStacksTransaction, + isMultisig, +}: StacksTransactionSignerProps) { + const transactionRequest = useTransactionRequestState(); + const { data: stxFees } = useCalculateStacksTxFees(stacksTransaction); + const analytics = useAnalytics(); + const { data: stacksBalances } = useCurrentStacksAccountAnchoredBalances(); + const navigate = useNavigate(); + const { data: nextNonce } = useNextNonce(); + const { search } = useLocation(); + + useRouteHeader(); + + useOnMount(() => { + void analytics.track('view_transaction_signing'), [analytics]; + }); + + const onSubmit = async (values: StacksTransactionFormValues) => { + onSignStacksTransaction(stxToMicroStx(values.fee).toNumber(), Number(values.nonce)); + }; + + if (!transactionRequest) return null; + + const validationSchema = + !transactionRequest.sponsored && !disableFeeSelection && !isMultisig + ? yup.object({ + fee: stxFeeValidator(stacksBalances?.stx.unlockedStx), + nonce: nonceValidator, + }) + : yup.object({ + nonce: nonceValidator, + }); + + const isNonceAlreadySet = !Number.isNaN(transactionRequest.nonce); + + const initialValues: StacksTransactionFormValues = { + fee: '', + feeCurrency: 'STX', + feeType: FeeTypes[FeeTypes.Middle], + nonce: isNonceAlreadySet ? transactionRequest.nonce : nextNonce?.nonce, + }; + + return ( + + + + + + + {transactionRequest.txType === 'contract_call' && } + {transactionRequest.txType === 'token_transfer' && } + {transactionRequest.txType === 'smart_contract' && } + + {() => ( + <> + {!isNonceAlreadySet && } + + {!disableNonceSelection && ( + navigate(RouteUrls.EditNonce + search)} + /> + )} + + + + + + )} + + + ); +} diff --git a/src/app/pages/transaction-request/components/stx-transfer-details/stx-transfer-details.tsx b/src/app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details.tsx similarity index 88% rename from src/app/pages/transaction-request/components/stx-transfer-details/stx-transfer-details.tsx rename to src/app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details.tsx index 70848d22fcd..0cafaa5012a 100644 --- a/src/app/pages/transaction-request/components/stx-transfer-details/stx-transfer-details.tsx +++ b/src/app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details.tsx @@ -2,8 +2,8 @@ import { Stack, color } from '@stacks/ui'; import { Divider } from '@app/components/layout/divider'; import { Title } from '@app/components/typography'; -import { AttachmentRow } from '@app/pages/transaction-request/components/attachment-row'; -import { Row } from '@app/pages/transaction-request/components/row'; +import { AttachmentRow } from '@app/features/stacks-transaction-request/attachment-row'; +import { Row } from '@app/features/stacks-transaction-request/row'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; export function StxTransferDetails(): React.JSX.Element | null { diff --git a/src/app/pages/transaction-request/components/submit-action.tsx b/src/app/features/stacks-transaction-request/submit-action.tsx similarity index 94% rename from src/app/pages/transaction-request/components/submit-action.tsx rename to src/app/features/stacks-transaction-request/submit-action.tsx index 43efc4b1468..0f4949a2a93 100644 --- a/src/app/pages/transaction-request/components/submit-action.tsx +++ b/src/app/features/stacks-transaction-request/submit-action.tsx @@ -10,7 +10,7 @@ import { isEmpty } from '@shared/utils'; import { useDrawers } from '@app/common/hooks/use-drawers'; import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { ButtonProps, LeatherButton } from '@app/components/button/button'; -import { useTransactionError } from '@app/pages/transaction-request/hooks/use-transaction-error'; +import { useTransactionError } from '@app/features/stacks-transaction-request/hooks/use-transaction-error'; function BaseConfirmButton(props: ButtonProps): React.JSX.Element { return ( diff --git a/src/app/pages/transaction-request/components/transaction-error/error-message.tsx b/src/app/features/stacks-transaction-request/transaction-error/error-message.tsx similarity index 100% rename from src/app/pages/transaction-request/components/transaction-error/error-message.tsx rename to src/app/features/stacks-transaction-request/transaction-error/error-message.tsx diff --git a/src/app/pages/transaction-request/components/transaction-error/error-messages.tsx b/src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx similarity index 98% rename from src/app/pages/transaction-request/components/transaction-error/error-messages.tsx rename to src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx index 97d052a587f..83cf5cc4f6b 100644 --- a/src/app/pages/transaction-request/components/transaction-error/error-messages.tsx +++ b/src/app/features/stacks-transaction-request/transaction-error/error-messages.tsx @@ -14,7 +14,7 @@ import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; import { stacksValue } from '@app/common/stacks-utils'; import { LeatherButton } from '@app/components/button/button'; import { Caption } from '@app/components/typography'; -import { ErrorMessage } from '@app/pages/transaction-request/components/transaction-error/error-message'; +import { ErrorMessage } from '@app/features/stacks-transaction-request/transaction-error/error-message'; import { useCurrentStacksAccountAnchoredBalances } from '@app/query/stacks/balance/stx-balance.hooks'; import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; diff --git a/src/app/pages/transaction-request/components/transaction-error/transaction-error.tsx b/src/app/features/stacks-transaction-request/transaction-error/transaction-error.tsx similarity index 94% rename from src/app/pages/transaction-request/components/transaction-error/transaction-error.tsx rename to src/app/features/stacks-transaction-request/transaction-error/transaction-error.tsx index b4544022d1c..2eafa04d6f1 100644 --- a/src/app/pages/transaction-request/components/transaction-error/transaction-error.tsx +++ b/src/app/features/stacks-transaction-request/transaction-error/transaction-error.tsx @@ -1,7 +1,7 @@ import { Suspense, memo, useEffect } from 'react'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; -import { useTransactionError } from '@app/pages/transaction-request/hooks/use-transaction-error'; +import { useTransactionError } from '@app/features/stacks-transaction-request/hooks/use-transaction-error'; import { ExpiredRequestErrorMessage, diff --git a/src/app/pages/rpc-sign-stacks-transaction/rpc-sign-stacks-transaction.tsx b/src/app/pages/rpc-sign-stacks-transaction/rpc-sign-stacks-transaction.tsx new file mode 100644 index 00000000000..ef424ee3532 --- /dev/null +++ b/src/app/pages/rpc-sign-stacks-transaction/rpc-sign-stacks-transaction.tsx @@ -0,0 +1,24 @@ +import { StacksTransactionSigner } from '@app/features/stacks-transaction-request/stacks-transaction-signer'; +import { useRpcSignStacksTransaction } from '@app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction'; + +export function RpcSignStacksTransaction() { + const { + onSignStacksTransaction, + onCancel, + disableFeeSelection, + stacksTransaction, + disableNonceSelection, + isMultisig, + } = useRpcSignStacksTransaction(); + + return ( + + ); +} diff --git a/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts b/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts new file mode 100644 index 00000000000..1498cc40575 --- /dev/null +++ b/src/app/pages/rpc-sign-stacks-transaction/use-rpc-sign-stacks-transaction.ts @@ -0,0 +1,84 @@ +import { useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import { RpcErrorCode } from '@btckit/types'; +import { bytesToHex } from '@stacks/common'; +import { MultiSigSpendingCondition, deserializeTransaction } from '@stacks/transactions'; + +import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods'; + +import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; +import { useRejectIfLedgerWallet } from '@app/common/rpc-helpers'; +import { useSignTransactionSoftwareWallet } from '@app/store/transactions/transaction.hooks'; + +function useRpcSignStacksTransactionParams() { + useRejectIfLedgerWallet('stx_signTransaction'); + + const [searchParams] = useSearchParams(); + const { origin, tabId } = useDefaultRequestParams(); + const requestId = searchParams.get('requestId'); + const txHex = searchParams.get('txHex'); + const isMultisig = searchParams.get('isMultisig'); + + if (!requestId || !txHex || !origin) throw new Error('Invalid params'); + + return useMemo( + () => ({ + origin, + tabId: tabId ?? 0, + requestId, + isMultisig: isMultisig === 'true', + stacksTransaction: deserializeTransaction(txHex), + }), + [origin, txHex, requestId, isMultisig, tabId] + ); +} + +export function useRpcSignStacksTransaction() { + const { origin, requestId, tabId, stacksTransaction, isMultisig } = + useRpcSignStacksTransactionParams(); + const signSoftwareWalletTx = useSignTransactionSoftwareWallet(); + const wasSignedByOtherOwners = + isMultisig && + (stacksTransaction.auth.spendingCondition as MultiSigSpendingCondition).fields?.length > 0; + + return { + origin, + disableFeeSelection: wasSignedByOtherOwners, + disableNonceSelection: wasSignedByOtherOwners, + stacksTransaction, + isMultisig, + onSignStacksTransaction(fee: number, nonce: number) { + stacksTransaction.setFee(fee); + stacksTransaction.setNonce(nonce); + + const signedTransaction = signSoftwareWalletTx(stacksTransaction); + if (!signedTransaction) { + throw new Error('Error signing stacks transaction'); + } + + chrome.tabs.sendMessage( + tabId, + makeRpcSuccessResponse('stx_signTransaction', { + id: requestId, + result: { + txHex: bytesToHex(signedTransaction.serialize()), + }, + }) + ); + window.close(); + }, + onCancel() { + chrome.tabs.sendMessage( + tabId, + makeRpcErrorResponse('stx_signTransaction', { + id: requestId, + error: { + message: 'User denied signing stacks transaction', + code: RpcErrorCode.USER_REJECTION, + }, + }) + ); + }, + }; +} diff --git a/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx b/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx index 8f33352d721..ecf44a7b924 100644 --- a/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx +++ b/src/app/pages/send/send-crypto-asset-form/form/stacks/stacks-common-send-form.tsx @@ -58,7 +58,8 @@ export function StacksCommonSendForm({ {props => { onFormStateChange(props.values); return ( - + <> +
{amountField} @@ -76,7 +77,7 @@ export function StacksCommonSendForm({ -
+ ); }} diff --git a/src/app/pages/transaction-request/transaction-request.tsx b/src/app/pages/transaction-request/transaction-request.tsx index 52e1da8ee20..c86651f30ab 100644 --- a/src/app/pages/transaction-request/transaction-request.tsx +++ b/src/app/pages/transaction-request/transaction-request.tsx @@ -24,13 +24,16 @@ import { PopupHeader } from '@app/features/current-account/popup-header'; import { RequestingTabClosedWarningMessage } from '@app/features/errors/requesting-tab-closed-error-msg'; import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer'; import { useLedgerNavigate } from '@app/features/ledger/hooks/use-ledger-navigate'; -import { ContractCallDetails } from '@app/pages/transaction-request/components/contract-call-details/contract-call-details'; -import { ContractDeployDetails } from '@app/pages/transaction-request/components/contract-deploy-details/contract-deploy-details'; -import { PageTop } from '@app/pages/transaction-request/components/page-top'; -import { PostConditionModeWarning } from '@app/pages/transaction-request/components/post-condition-mode-warning'; -import { PostConditions } from '@app/pages/transaction-request/components/post-conditions/post-conditions'; -import { StxTransferDetails } from '@app/pages/transaction-request/components/stx-transfer-details/stx-transfer-details'; -import { TransactionError } from '@app/pages/transaction-request/components/transaction-error/transaction-error'; +import { ContractCallDetails } from '@app/features/stacks-transaction-request/contract-call-details/contract-call-details'; +import { ContractDeployDetails } from '@app/features/stacks-transaction-request/contract-deploy-details/contract-deploy-details'; +import { FeeForm } from '@app/features/stacks-transaction-request/fee-form'; +import { MinimalErrorMessage } from '@app/features/stacks-transaction-request/minimal-error-message'; +import { PageTop } from '@app/features/stacks-transaction-request/page-top'; +import { PostConditionModeWarning } from '@app/features/stacks-transaction-request/post-condition-mode-warning'; +import { PostConditions } from '@app/features/stacks-transaction-request/post-conditions/post-conditions'; +import { StxTransferDetails } from '@app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details'; +import { SubmitAction } from '@app/features/stacks-transaction-request/submit-action'; +import { TransactionError } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; import { useCurrentStacksAccountAnchoredBalances } from '@app/query/stacks/balance/stx-balance.hooks'; import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks'; import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks'; @@ -41,10 +44,6 @@ import { useUnsignedStacksTransactionBaseState, } from '@app/store/transactions/transaction.hooks'; -import { FeeForm } from './components/fee-form'; -import { MinimalErrorMessage } from './components/minimal-error-message'; -import { SubmitAction } from './components/submit-action'; - function TransactionRequestBase() { const transactionRequest = useTransactionRequestState(); const { setIsLoading, setIsIdle } = useLoading(LoadingKeys.SUBMIT_TRANSACTION); @@ -125,17 +124,16 @@ function TransactionRequestBase() { > {() => ( <> - - - navigate(RouteUrls.EditNonce)} - /> - - - - + + + navigate(RouteUrls.EditNonce)} + /> + + + )} diff --git a/src/app/query/common/outdated-pr/outdated-pr.query.ts b/src/app/query/common/outdated-pr/outdated-pr.query.ts new file mode 100644 index 00000000000..b1e7b60b98e --- /dev/null +++ b/src/app/query/common/outdated-pr/outdated-pr.query.ts @@ -0,0 +1,37 @@ +import type { Endpoints } from '@octokit/types'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; + +import { GITHUB_ORG, GITHUB_REPO } from '@shared/constants'; +import { COMMIT_SHA, PR_NUMBER } from '@shared/environment'; +import { isDefined } from '@shared/utils'; + +type PrDetailsResp = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}']['response']['data']; + +async function getPullRequestDetails(pr: string): Promise { + const resp = await axios.get( + `https://api.github.com/repos/${GITHUB_ORG}/${GITHUB_REPO}/pulls/${pr}` + ); + return resp.data; +} + +function usePullRequestDetailsQuery() { + return useQuery({ + enabled: isDefined(PR_NUMBER) && isDefined(COMMIT_SHA), + queryKey: ['pull-request-details', PR_NUMBER], + async queryFn() { + return getPullRequestDetails(PR_NUMBER ?? ''); + }, + }); +} + +export function useIsLatestPullRequestBuild() { + const { data: pullRequest } = usePullRequestDetailsQuery(); + if (!pullRequest) return { isLatestBuild: true }; + return { + // If the latest commit SHA on the PR is not the same one used for this build, + // we can assume it's outdated + isLatestBuild: pullRequest.head.sha.startsWith(COMMIT_SHA ?? ''), + pullRequestLink: pullRequest.html_url, + }; +} diff --git a/src/app/routes/app-routes.tsx b/src/app/routes/app-routes.tsx index 9db1689f590..1ef80f077f9 100644 --- a/src/app/routes/app-routes.tsx +++ b/src/app/routes/app-routes.tsx @@ -45,6 +45,7 @@ import { RpcGetAddresses } from '@app/pages/rpc-get-addresses/rpc-get-addresses' 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'; +import { RpcSignStacksTransaction } from '@app/pages/rpc-sign-stacks-transaction/rpc-sign-stacks-transaction'; import { SelectNetwork } from '@app/pages/select-network/select-network'; import { BroadcastError } from '@app/pages/send/broadcast-error/broadcast-error'; import { LockBitcoinSummary } from '@app/pages/send/locked-bitcoin-summary/locked-bitcoin-summary'; @@ -342,6 +343,17 @@ function useAppRoutes() { } /> + + + + } + > + } /> + + { diff --git a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts index e8d872f3161..f3863e6ae63 100644 --- a/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts +++ b/src/app/store/accounts/blockchain/bitcoin/bitcoin-keychain.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { createSelector } from '@reduxjs/toolkit'; import { HDKey, Versions } from '@scure/bip32'; -import { NetworkModes } from '@shared/constants'; +import { BitcoinNetworkModes } from '@shared/constants'; import { getBtcSignerLibNetworkConfigByMode } from '@shared/crypto/bitcoin/bitcoin.network'; import { BitcoinAccount, @@ -25,11 +25,11 @@ import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; export function bitcoinAccountBuilderFactory( softwareKeychainDerivationFn: ( key: HDKey, - network: NetworkModes + network: BitcoinNetworkModes ) => (accountIndex: number) => BitcoinAccount, ledgerKeychainLookupFn: ( keyMap: Record, - network: NetworkModes + network: BitcoinNetworkModes ) => (accountIndex: number) => BitcoinAccount | undefined ) { return createSelector( @@ -42,12 +42,16 @@ export function bitcoinAccountBuilderFactory( return { mainnet: ledgerKeychainLookupFn(bitcoinLedgerKeys, 'mainnet'), testnet: ledgerKeychainLookupFn(bitcoinLedgerKeys, 'testnet'), + signet: ledgerKeychainLookupFn(bitcoinLedgerKeys, 'signet'), + regtest: ledgerKeychainLookupFn(bitcoinLedgerKeys, 'regtest'), }; } if (!rootKeychain) throw new Error('No in-memory key found'); return { mainnet: softwareKeychainDerivationFn(rootKeychain, 'mainnet'), testnet: softwareKeychainDerivationFn(rootKeychain, 'testnet'), + signet: softwareKeychainDerivationFn(rootKeychain, 'signet'), + regtest: softwareKeychainDerivationFn(rootKeychain, 'regtest'), }; } ); diff --git a/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts index decaef452bb..f7bd64fc2a1 100644 --- a/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks.ts @@ -4,7 +4,6 @@ import { useSelector } from 'react-redux'; import { createSelector } from '@reduxjs/toolkit'; import { - bitcoinNetworkModeToCoreNetworkMode, deriveAddressIndexZeroFromAccount, lookUpLedgerKeysByPath, } from '@shared/crypto/bitcoin/bitcoin.utils'; @@ -36,8 +35,7 @@ const selectNativeSegwitAccountBuilder = bitcoinAccountBuilderFactory( const selectCurrentNetworkNativeSegwitAccountBuilder = createSelector( selectNativeSegwitAccountBuilder, selectCurrentNetwork, - (nativeSegwitKeychain, network) => - nativeSegwitKeychain[bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network)] + (nativeSegwitKeychains, network) => nativeSegwitKeychains[network.chain.bitcoin.network] ); export function useNativeSegwitAccountBuilder() { diff --git a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts index d7f8b7c1c6a..3822447a22e 100644 --- a/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts +++ b/src/app/store/accounts/blockchain/bitcoin/taproot-account.hooks.ts @@ -4,10 +4,7 @@ import { useSelector } from 'react-redux'; import { createSelector } from '@reduxjs/toolkit'; import { BitcoinNetworkModes } from '@shared/constants'; -import { - bitcoinNetworkModeToCoreNetworkMode, - lookUpLedgerKeysByPath, -} from '@shared/crypto/bitcoin/bitcoin.utils'; +import { lookUpLedgerKeysByPath } from '@shared/crypto/bitcoin/bitcoin.utils'; import { deriveTaprootAccount, getTaprootAccountDerivationPath, @@ -35,8 +32,7 @@ const selectTaprootAccountBuilder = bitcoinAccountBuilderFactory( const selectCurrentNetworkTaprootAccountBuilder = createSelector( selectTaprootAccountBuilder, selectCurrentNetwork, - (taprootKeychain, network) => - taprootKeychain[bitcoinNetworkModeToCoreNetworkMode(network.chain.bitcoin.network)] + (taprootKeychains, network) => taprootKeychains[network.chain.bitcoin.network] ); const selectCurrentTaprootAccount = createSelector( selectCurrentNetworkTaprootAccountBuilder, diff --git a/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts b/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts index ab866a0871e..e343f716d3a 100644 --- a/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts +++ b/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts @@ -13,6 +13,7 @@ import { deriveStxPrivateKey, generateWallet } from '@stacks/wallet-sdk'; import { atom } from 'jotai'; import { DATA_DERIVATION_PATH, deriveStacksSalt } from '@shared/crypto/stacks/stacks-address-gen'; +import { defaultWalletKeyId } from '@shared/utils'; import { derivePublicKey } from '@app/common/keychain/keychain'; import { createNullArrayOfLength } from '@app/common/utils'; @@ -25,7 +26,6 @@ import { import { selectLedgerKey } from '@app/store/keys/key.selectors'; import { addressNetworkVersionState } from '@app/store/transactions/transaction'; -import { defaultKeyId } from '../../../keys/key.slice'; import { HardwareStacksAccount, SoftwareStacksAccount, @@ -62,7 +62,7 @@ const selectStacksWalletState = createSelector( selectStacksChain, (keychain, chain) => { if (!keychain) return; - const { highestAccountIndex, currentAccountIndex } = chain[defaultKeyId]; + const { highestAccountIndex, currentAccountIndex } = chain[defaultWalletKeyId]; const numberOfAccountsToDerive = Math.max(highestAccountIndex, currentAccountIndex) + 1; return createNullArrayOfLength(numberOfAccountsToDerive).map((_, index) => initalizeStacksAccount(keychain, index) diff --git a/src/app/store/chains/stx-chain.slice.ts b/src/app/store/chains/stx-chain.slice.ts index 4412b542ffb..e113860285c 100644 --- a/src/app/store/chains/stx-chain.slice.ts +++ b/src/app/store/chains/stx-chain.slice.ts @@ -1,6 +1,8 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import { defaultKeyId, keySlice } from '../keys/key.slice'; +import { defaultWalletKeyId } from '@shared/utils'; + +import { keySlice } from '../keys/key.slice'; interface StxChainKeyState { highestAccountIndex: number; @@ -8,7 +10,7 @@ interface StxChainKeyState { } const initialState: Record = { - [defaultKeyId]: { + [defaultWalletKeyId]: { highestAccountIndex: 0, currentAccountIndex: 0, }, diff --git a/src/app/store/in-memory-key/in-memory-key.selectors.ts b/src/app/store/in-memory-key/in-memory-key.selectors.ts index 80bd6032180..0a9ef5ee5f2 100644 --- a/src/app/store/in-memory-key/in-memory-key.selectors.ts +++ b/src/app/store/in-memory-key/in-memory-key.selectors.ts @@ -2,16 +2,17 @@ import { useSelector } from 'react-redux'; import { createSelector } from '@reduxjs/toolkit'; +import { defaultWalletKeyId } from '@shared/utils'; + import { mnemonicToRootNode } from '@app/common/keychain/keychain'; import { RootState } from '..'; -import { defaultKeyId } from '../keys/key.slice'; const selectInMemoryKey = (state: RootState) => state.inMemoryKeys; export const selectDefaultWalletKey = createSelector( selectInMemoryKey, - state => state.keys[defaultKeyId] + state => state.keys[defaultWalletKeyId] ); export const selectRootKeychain = createSelector(selectDefaultWalletKey, key => { diff --git a/src/app/store/in-memory-key/in-memory-key.slice.ts b/src/app/store/in-memory-key/in-memory-key.slice.ts index f95ef155d7e..2daf784c312 100644 --- a/src/app/store/in-memory-key/in-memory-key.slice.ts +++ b/src/app/store/in-memory-key/in-memory-key.slice.ts @@ -1,8 +1,9 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { logger } from '@shared/logger'; +import { defaultWalletKeyId } from '@shared/utils'; -import { defaultKeyId, keySlice } from '../keys/key.slice'; +import { keySlice } from '../keys/key.slice'; interface InMemoryKeyState { hasRestoredKeys: boolean; @@ -20,15 +21,15 @@ export const inMemoryKeySlice = createSlice({ reducers: { generateWalletKey(state, action: PayloadAction) { - if (state.keys[defaultKeyId]) { + if (state.keys[defaultWalletKeyId]) { logger.warn('Not generating another wallet, already exists.'); return; } - state.keys[defaultKeyId] = action.payload; + state.keys[defaultWalletKeyId] = action.payload; }, saveUsersSecretKeyToBeRestored(state, action: PayloadAction) { - state.keys[defaultKeyId] = action.payload; + state.keys[defaultWalletKeyId] = action.payload; }, setKeysInMemory(state, action: PayloadAction>) { diff --git a/src/app/store/keys/key.actions.ts b/src/app/store/keys/key.actions.ts index 71c92cce452..fc303d57eb6 100644 --- a/src/app/store/keys/key.actions.ts +++ b/src/app/store/keys/key.actions.ts @@ -2,6 +2,7 @@ import { AddressVersion } from '@stacks/transactions'; import { decryptMnemonic, encryptMnemonic } from '@shared/crypto/mnemonic-encryption'; import { logger } from '@shared/logger'; +import { defaultWalletKeyId } from '@shared/utils'; import { identifyUser } from '@shared/utils/analytics'; import { recurseAccountsForActivity } from '@app/common/account-restoration/account-restore'; @@ -19,7 +20,7 @@ import { stxChainSlice } from '../chains/stx-chain.slice'; import { selectDefaultWalletKey } from '../in-memory-key/in-memory-key.selectors'; import { inMemoryKeySlice } from '../in-memory-key/in-memory-key.slice'; import { selectCurrentKey } from './key.selectors'; -import { defaultKeyId, keySlice } from './key.slice'; +import { keySlice } from './key.slice'; function setWalletEncryptionPassword(args: { password: string; @@ -86,7 +87,7 @@ function setWalletEncryptionPassword(args: { dispatch( keySlice.actions.createStacksSoftwareWalletComplete({ type: 'software', - id: defaultKeyId, + id: defaultWalletKeyId, salt, encryptedSecretKey, }) diff --git a/src/app/store/keys/key.selectors.ts b/src/app/store/keys/key.selectors.ts index be2e9db854a..c4c794da3b1 100644 --- a/src/app/store/keys/key.selectors.ts +++ b/src/app/store/keys/key.selectors.ts @@ -2,18 +2,19 @@ import { useSelector } from 'react-redux'; import { createSelector } from '@reduxjs/toolkit'; +import { defaultWalletKeyId } from '@shared/utils'; + import { initialSearchParams } from '@app/common/initial-search-params'; import { initBigNumber } from '@app/common/math/helpers'; import { RootState } from '@app/store'; import { selectStacksChain } from '../chains/stx-chain.selectors'; -import { defaultKeyId } from './key.slice'; const selectKeysSlice = (state: RootState) => state.keys; export const selectCurrentKey = createSelector( selectKeysSlice, - state => state.entities[defaultKeyId] + state => state.entities[defaultWalletKeyId] ); export function useCurrentKeyDetails() { @@ -25,7 +26,7 @@ export const selectCurrentAccountIndex = createSelector(selectStacksChain, state if (customAccountIndex && initBigNumber(customAccountIndex).isInteger()) { return initBigNumber(customAccountIndex).toNumber(); } - return state[defaultKeyId].currentAccountIndex; + return state[defaultWalletKeyId].currentAccountIndex; }); export const selectLedgerKey = createSelector(selectKeysSlice, keys => { diff --git a/src/app/store/keys/key.slice.ts b/src/app/store/keys/key.slice.ts index 1e2fc8a79a6..e99e5bb0a90 100644 --- a/src/app/store/keys/key.slice.ts +++ b/src/app/store/keys/key.slice.ts @@ -1,11 +1,11 @@ import { PayloadAction, createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { defaultWalletKeyId } from '@shared/utils'; + import { StxAndIdentityPublicKeys } from '@app/features/ledger/utils/stacks-ledger-utils'; import { migrateVaultReducerStoreToNewStateStructure } from '../utils/vault-reducer-migration'; -export const defaultKeyId = 'default' as const; - interface KeyConfigSoftware { type: 'software'; id: string; @@ -38,7 +38,7 @@ export const keySlice = createSlice({ }, signOut(state) { - keyAdapter.removeOne(state, defaultKeyId); + keyAdapter.removeOne(state, defaultWalletKeyId); }, debugKillStacks(state) { diff --git a/src/app/store/ledger/bitcoin-key.slice.ts b/src/app/store/ledger/bitcoin-key.slice.ts index 8a3a36f32f4..6995edbbc54 100644 --- a/src/app/store/ledger/bitcoin-key.slice.ts +++ b/src/app/store/ledger/bitcoin-key.slice.ts @@ -1,9 +1,10 @@ import { PayloadAction, createEntityAdapter, createSlice } from '@reduxjs/toolkit'; +import { defaultWalletKeyId } from '@shared/utils'; + import { BitcoinLedgerAccountDetails } from '@app/features/ledger/utils/bitcoin-ledger-utils'; import { RootState } from '..'; -import { defaultKeyId } from '../keys/key.slice'; interface PersistedBitcoinKeys extends BitcoinLedgerAccountDetails { walletId: string; @@ -19,7 +20,7 @@ export const bitcoinKeysSlice = createSlice({ bitcoinKeyAdapter.addMany( state, // While we only support a single wallet, we default to the `default` walletId - payload.map(key => ({ ...key, walletId: defaultKeyId })) + payload.map(key => ({ ...key, walletId: defaultWalletKeyId })) ); }, }, diff --git a/src/app/store/transactions/requests.hooks.ts b/src/app/store/transactions/requests.hooks.ts index 0a96c6b14ef..1adf80e6b16 100644 --- a/src/app/store/transactions/requests.hooks.ts +++ b/src/app/store/transactions/requests.hooks.ts @@ -10,8 +10,11 @@ export function useTransactionRequest() { export function useTransactionRequestState() { const requestToken = useTransactionRequest(); + return useMemo(() => { - if (!requestToken) return null; + if (!requestToken) { + return null; + } return getPayloadFromToken(requestToken); }, [requestToken]); } diff --git a/src/app/store/utils/vault-reducer-migration.spec.ts b/src/app/store/utils/vault-reducer-migration.spec.ts index ca09465ca3a..584c6b76f5f 100644 --- a/src/app/store/utils/vault-reducer-migration.spec.ts +++ b/src/app/store/utils/vault-reducer-migration.spec.ts @@ -1,7 +1,8 @@ import { LocalStorageMock } from '@tests/unit/local-storage-mock'; import { vi } from 'vitest'; -import { defaultKeyId } from '../keys/key.slice'; +import { defaultWalletKeyId } from '@shared/utils'; + import { migrateVaultReducerStoreToNewStateStructure } from './vault-reducer-migration'; (globalThis as any).localStorage = new LocalStorageMock(); @@ -32,11 +33,11 @@ describe(migrateVaultReducerStoreToNewStateStructure.name, () => { test('that it returns a migrated state object when wallet values are detected', () => { const returnedValue = migrateVaultReducerStoreToNewStateStructure({} as any); expect(returnedValue).toEqual({ - ids: [defaultKeyId], + ids: [defaultWalletKeyId], entities: { - [defaultKeyId]: { + [defaultWalletKeyId]: { type: 'software', - id: defaultKeyId, + id: defaultWalletKeyId, encryptedSecretKey: 'test-encrypted-key', salt: 'test-salt', }, diff --git a/src/background/messaging/rpc-message-handler.ts b/src/background/messaging/rpc-message-handler.ts index bddeb907c35..5856cc955d8 100644 --- a/src/background/messaging/rpc-message-handler.ts +++ b/src/background/messaging/rpc-message-handler.ts @@ -2,6 +2,8 @@ import { RpcErrorCode } from '@btckit/types'; import { WalletRequests, makeRpcErrorResponse } from '@shared/rpc/rpc-methods'; +import { rpcSignStacksTransaction } from '@background/messaging/rpc-methods/sign-stacks-transaction'; + import { getTabIdFromPort } from './messaging-utils'; import { rpcAcceptBitcoinContractOffer } from './rpc-methods/accept-bitcoin-contract'; import { rpcGetAddresses } from './rpc-methods/get-addresses'; @@ -32,6 +34,11 @@ export async function rpcMessageHandler(message: WalletRequests, port: chrome.ru break; } + case 'stx_signTransaction': { + await rpcSignStacksTransaction(message, port); + break; + } + case 'supportedMethods': { rpcSupportedMethods(message, port); break; diff --git a/src/background/messaging/rpc-methods/sign-stacks-transaction.ts b/src/background/messaging/rpc-methods/sign-stacks-transaction.ts new file mode 100644 index 00000000000..453f0cfd96f --- /dev/null +++ b/src/background/messaging/rpc-methods/sign-stacks-transaction.ts @@ -0,0 +1,188 @@ +import { RpcErrorCode } from '@btckit/types'; +import { bytesToHex } from '@stacks/common'; +import { TransactionTypes } from '@stacks/connect'; +import { + AddressHashMode, + AuthType, + MultiSigHashMode, + PayloadType, + PostCondition, + StacksTransaction, + VersionedSmartContractPayload, + addressToString, + cvToValue, + deserializeTransaction, + serializeCV, + serializePostCondition, +} from '@stacks/transactions'; +import BigNumber from 'bignumber.js'; +import { createUnsecuredToken } from 'jsontokens'; + +import { STX_DECIMALS } from '@shared/constants'; +import { RouteUrls } from '@shared/route-urls'; +import { + SignStacksTransactionRequest, + getRpcSignStacksTransactionParamErrors, + validateRpcSignStacksTransactionParams, +} from '@shared/rpc/methods/sign-stacks-transaction'; +import { makeRpcErrorResponse } from '@shared/rpc/rpc-methods'; +import { isDefined, isUndefined } from '@shared/utils'; + +import { + RequestParams, + getTabIdFromPort, + listenForPopupClose, + makeSearchParamsWithDefaults, + triggerRequestWindowOpen, +} from '../messaging-utils'; + +const MEMO_DESERIALIZATION_STUB = '\u0000'; + +const cleanMemoString = (memo: string): string => { + return memo.replaceAll(MEMO_DESERIALIZATION_STUB, ''); +}; + +function encodePostConditions(postConditions: PostCondition[]) { + return postConditions.map(pc => bytesToHex(serializePostCondition(pc))); +} + +const transactionPayloadToTransactionRequest = ( + stacksTransaction: StacksTransaction, + stxAddress?: string, + attachment?: string +) => { + const transactionRequest = { + attachment, + stxAddress, + sponsored: stacksTransaction.auth.authType === AuthType.Sponsored, + nonce: Number(stacksTransaction.auth.spendingCondition.nonce), + fee: Number(stacksTransaction.auth.spendingCondition.fee), + postConditions: encodePostConditions(stacksTransaction.postConditions.values as any[]), + postConditionMode: stacksTransaction.postConditionMode, + anchorMode: stacksTransaction.anchorMode, + } as any; + + switch (stacksTransaction.payload.payloadType) { + case PayloadType.TokenTransfer: + transactionRequest.txType = TransactionTypes.STXTransfer; + transactionRequest.recipient = cvToValue(stacksTransaction.payload.recipient, true); + transactionRequest.amount = new BigNumber(Number(stacksTransaction.payload.amount)) + .shiftedBy(-STX_DECIMALS) + .toNumber() + .toLocaleString('en-US', { maximumFractionDigits: STX_DECIMALS }); + transactionRequest.memo = cleanMemoString(stacksTransaction.payload.memo.content); + break; + case PayloadType.ContractCall: + transactionRequest.txType = TransactionTypes.ContractCall; + transactionRequest.contractName = stacksTransaction.payload.contractName.content; + transactionRequest.contractAddress = addressToString( + stacksTransaction.payload.contractAddress + ); + transactionRequest.functionArgs = stacksTransaction.payload.functionArgs.map(arg => + Buffer.from(serializeCV(arg)).toString('hex') + ); + transactionRequest.functionName = stacksTransaction.payload.functionName.content; + break; + case PayloadType.SmartContract: + case PayloadType.VersionedSmartContract: + transactionRequest.txType = TransactionTypes.ContractDeploy; + transactionRequest.contractName = stacksTransaction.payload.contractName.content; + transactionRequest.codeBody = stacksTransaction.payload.codeBody.content; + transactionRequest.clarityVersion = ( + stacksTransaction.payload as VersionedSmartContractPayload + ).clarityVersion; + break; + default: + throw new Error('Unsupported tx type'); + } + + return transactionRequest; +}; + +function validateStacksTransaction(txHex: string) { + try { + deserializeTransaction(txHex); + return true; + } catch (e) { + return false; + } +} + +export async function rpcSignStacksTransaction( + message: SignStacksTransactionRequest, + port: chrome.runtime.Port +) { + if (isUndefined(message.params)) { + chrome.tabs.sendMessage( + getTabIdFromPort(port), + makeRpcErrorResponse('stx_signTransaction', { + id: message.id, + error: { code: RpcErrorCode.INVALID_REQUEST, message: 'Parameters undefined' }, + }) + ); + return; + } + + if (!validateRpcSignStacksTransactionParams(message.params)) { + chrome.tabs.sendMessage( + getTabIdFromPort(port), + makeRpcErrorResponse('stx_signTransaction', { + id: message.id, + error: { + code: RpcErrorCode.INVALID_PARAMS, + message: getRpcSignStacksTransactionParamErrors(message.params), + }, + }) + ); + return; + } + + if (!validateStacksTransaction(message.params.txHex!)) { + chrome.tabs.sendMessage( + getTabIdFromPort(port), + makeRpcErrorResponse('stx_signTransaction', { + id: message.id, + error: { code: RpcErrorCode.INVALID_PARAMS, message: 'Invalid Stacks transaction hex' }, + }) + ); + return; + } + + const stacksTransaction = deserializeTransaction(message.params.txHex!); + const request = transactionPayloadToTransactionRequest( + stacksTransaction, + message.params.stxAddress, + message.params.attachment + ); + + const hashMode = stacksTransaction.auth.spendingCondition.hashMode as MultiSigHashMode; + const isMultisig = + hashMode === AddressHashMode.SerializeP2SH || hashMode === AddressHashMode.SerializeP2WSH; + + const requestParams = [ + ['txHex', message.params.txHex], + ['requestId', message.id], + ['request', createUnsecuredToken(request)], + ['isMultisig', isMultisig], + ] as RequestParams; + + if (isDefined(message.params.network)) { + requestParams.push(['network', message.params.network]); + } + + const { urlParams, tabId } = makeSearchParamsWithDefaults(port, requestParams); + + const { id } = await triggerRequestWindowOpen(RouteUrls.RpcSignStacksTransaction, urlParams); + + listenForPopupClose({ + tabId, + id, + response: makeRpcErrorResponse('stx_signTransaction', { + id: message.id, + error: { + code: RpcErrorCode.USER_REJECTION, + message: 'User rejected the Stacks transaction signing request', + }, + }), + }); +} diff --git a/src/shared/constants.ts b/src/shared/constants.ts index de5b3469a13..c6ccd8fd1a0 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -26,8 +26,8 @@ export const KEBAB_REGEX = /[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g; export const MICROBLOCKS_ENABLED = !IS_TEST_ENV && true; -export const GITHUB_ORG = 'hirosystems'; -export const GITHUB_REPO = 'wallet'; +export const GITHUB_ORG = 'leather-wallet'; +export const GITHUB_REPO = 'extension'; export enum WalletDefaultNetworkConfigurationIds { mainnet = 'mainnet', diff --git a/src/shared/crypto/bitcoin/bitcoin.utils.ts b/src/shared/crypto/bitcoin/bitcoin.utils.ts index 5b9b2e27592..4cb2705f81e 100644 --- a/src/shared/crypto/bitcoin/bitcoin.utils.ts +++ b/src/shared/crypto/bitcoin/bitcoin.utils.ts @@ -5,6 +5,7 @@ import * as btc from '@scure/btc-signer'; import { BitcoinNetworkModes, NetworkModes } from '@shared/constants'; import { whenNetwork } from '@shared/utils'; +import { defaultWalletKeyId } from '@shared/utils'; import { DerivationPathDepth } from '../derivation-path.utils'; import { BtcSignerNetwork } from './bitcoin.network'; @@ -173,13 +174,16 @@ export function getHdKeyVersionsFromNetwork(network: NetworkModes) { // Ledger wallets are keyed by their derivation path. To reuse the look up logic // between payment types, this factory fn accepts a fn that generates the path export function lookUpLedgerKeysByPath( - derivationPathFn: (network: BitcoinNetworkModes, accountIndex: number) => string + getDerivationPath: (network: BitcoinNetworkModes, accountIndex: number) => string ) { - return (keyMap: Record, network: NetworkModes) => + return ( + ledgerKeyMap: Record, + network: BitcoinNetworkModes + ) => (accountIndex: number) => { - const path = derivationPathFn(network, accountIndex); + const path = getDerivationPath(network, accountIndex); // Single wallet mode, hardcoded default walletId - const account = keyMap[path.replace('m', 'default')]; + const account = ledgerKeyMap[path.replace('m', defaultWalletKeyId)]; if (!account) return; return initBitcoinAccount(path, account.policy); }; diff --git a/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts b/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts index cf288af4a05..e21d418c608 100644 --- a/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts +++ b/src/shared/crypto/bitcoin/p2wpkh-address-gen.ts @@ -1,7 +1,7 @@ import { HDKey } from '@scure/bip32'; import * as btc from '@scure/btc-signer'; -import { BitcoinNetworkModes, NetworkModes } from '@shared/constants'; +import { BitcoinNetworkModes } from '@shared/constants'; import { DerivationPathDepth } from '../derivation-path.utils'; import { getBtcSignerLibNetworkConfigByMode } from './bitcoin.network'; @@ -26,7 +26,10 @@ export function getNativeSegwitAddressIndexDerivationPath( return getNativeSegwitAccountDerivationPath(network, accountIndex) + `/0/${addressIndex}`; } -export function deriveNativeSegwitAccountFromRootKeychain(keychain: HDKey, network: NetworkModes) { +export function deriveNativeSegwitAccountFromRootKeychain( + keychain: HDKey, + network: BitcoinNetworkModes +) { if (keychain.depth !== DerivationPathDepth.Root) throw new Error('Keychain passed is not a root'); return (accountIndex: number): BitcoinAccount => ({ type: 'p2wpkh', diff --git a/src/shared/environment.ts b/src/shared/environment.ts index 84bb4bc6904..e616ba71017 100644 --- a/src/shared/environment.ts +++ b/src/shared/environment.ts @@ -1,7 +1,8 @@ export const BRANCH = process.env.GITHUB_REF; -export const BRANCH_NAME = process.env.GITHUB_HEAD_REF; +export const BRANCH_NAME = process.env.GITHUB_HEAD_REF ?? process.env.BRANCH_NAME; +export const PR_NUMBER = process.env.PR_NUMBER; export const COINBASE_APP_ID = process.env.COINBASE_APP_ID ?? ''; -export const COMMIT_SHA = process.env.GITHUB_SHA; +export const COMMIT_SHA = process.env.COMMIT_SHA; export const IS_DEV_ENV = process.env.WALLET_ENVIRONMENT === 'development'; export const IS_TEST_ENV = process.env.WALLET_ENVIRONMENT === 'testing'; export const MOONPAY_API_KEY = process.env.MOONPAY_API_KEY ?? ''; diff --git a/src/shared/route-urls.ts b/src/shared/route-urls.ts index 85b76f5a1cb..266fe6478c7 100644 --- a/src/shared/route-urls.ts +++ b/src/shared/route-urls.ts @@ -97,7 +97,7 @@ export enum RouteUrls { TransactionRequest = '/transaction', TransactionBroadcastError = 'broadcast-error', - // Rpc request routes + // Request routes bitcoin RpcGetAddresses = '/get-addresses', RpcSignPsbt = '/sign-psbt', RpcSignPsbtSummary = '/sign-psbt/summary', @@ -111,4 +111,7 @@ export enum RouteUrls { // Shared legacy and rpc request routes RequestError = '/request-error', UnauthorizedRequest = '/unauthorized-request', + + // Request routes stacks + RpcSignStacksTransaction = '/sign-stacks-transaction', } diff --git a/src/shared/rpc/methods/sign-stacks-transaction.ts b/src/shared/rpc/methods/sign-stacks-transaction.ts new file mode 100644 index 00000000000..a950788f64a --- /dev/null +++ b/src/shared/rpc/methods/sign-stacks-transaction.ts @@ -0,0 +1,36 @@ +import { DefineRpcMethod, RpcRequest, RpcResponse } from '@btckit/types'; +import { StacksNetworks } from '@stacks/network'; +import * as yup from 'yup'; + +import { formatValidationErrors, getRpcParamErrors, validateRpcParams } from './validation.utils'; + +const rpcSignStacksTransactionParamsSchema = yup.object().shape({ + stxAddress: yup.string(), + txHex: yup.string().required(), + attachment: yup.string(), + network: yup.string().oneOf(StacksNetworks), +}); + +export function validateRpcSignStacksTransactionParams(obj: unknown) { + return validateRpcParams(obj, rpcSignStacksTransactionParamsSchema); +} + +export function getRpcSignStacksTransactionParamErrors(obj: unknown) { + return formatValidationErrors(getRpcParamErrors(obj, rpcSignStacksTransactionParamsSchema)); +} + +type SignStacksTransactionRequestParams = yup.InferType< + typeof rpcSignStacksTransactionParamsSchema +>; + +export type SignStacksTransactionRequest = RpcRequest< + 'stx_signTransaction', + SignStacksTransactionRequestParams +>; + +type SignStacksTransactionResponse = RpcResponse<{ txHex: string }>; + +export type SignStacksTransaction = DefineRpcMethod< + SignStacksTransactionRequest, + SignStacksTransactionResponse +>; diff --git a/src/shared/rpc/rpc-methods.ts b/src/shared/rpc/rpc-methods.ts index 87f2d7c7214..7384565e5c7 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 { SignStacksTransaction } from '@shared/rpc/methods/sign-stacks-transaction'; import { ValueOf } from '@shared/utils/type-utils'; import { AcceptBitcoinContract } from './methods/accept-bitcoin-contract'; @@ -7,7 +8,11 @@ import { SignPsbt } from './methods/sign-psbt'; import { SupportedMethods } from './methods/supported-methods'; // Supports BtcKit methods, as well as custom Leather methods -export type WalletMethodMap = BtcKitMethodMap & SupportedMethods & SignPsbt & AcceptBitcoinContract; +export type WalletMethodMap = BtcKitMethodMap & + SupportedMethods & + SignPsbt & + AcceptBitcoinContract & + SignStacksTransaction; export type WalletRequests = ValueOf['request']; export type WalletResponses = ValueOf['response']; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 77611acbe31..b237eb3f168 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -55,3 +55,5 @@ export function whenNetwork(mode: NetworkModes) { export function isEmptyArray(data: unknown[]) { return data.length === 0; } + +export const defaultWalletKeyId = 'default' as const; diff --git a/tests/mocks/constants.ts b/tests/mocks/constants.ts index e726add1149..ccc643142b8 100644 --- a/tests/mocks/constants.ts +++ b/tests/mocks/constants.ts @@ -5,12 +5,15 @@ export const TEST_ACCOUNT_1_TAPROOT_ADDRESS = // export const TEST_TESTNET_ACCOUNT_1_BTC_ADDRESS = 'tb1q3c7zyg58dd9hy07m77dv8es9vnpk8xad0yaw8y' export const TEST_ACCOUNT_1_STX_ADDRESS = 'SPS8CKF63P16J28AYF7PXW9E5AACH0NZNTEFWSFE'; -// export const TEST_ACCOUNT_1_PUBKEY = -// '02b6b0afe5f620bc8e532b640b148dd9dea0ed19d11f8ab420fcce488fe3974893'; +export const TEST_ACCOUNT_1_PUBKEY = + '02b6b0afe5f620bc8e532b640b148dd9dea0ed19d11f8ab420fcce488fe3974893'; export const TEST_ACCOUNT_2_STX_ADDRESS = 'SPRE7HABZGQ204G3VQAKMDMVBBD8A8CYG6BQKHQ'; export const TEST_TESTNET_ACCOUNT_2_STX_ADDRESS = 'STXH3HNBPM5YP15VH16ZXZ9AX6CK289K3NVR9T1P'; // export const TEST_ACCOUNT_2_BTC_ADDRESS = 'bc1qznkpz8fk07nmdhvr2k4nnea5n08tw6tk540snu'; export const TEST_TESTNET_ACCOUNT_2_BTC_ADDRESS = 'tb1qkzvk9hr7uvas23hspvsgqfvyc8h4nngeqjqtnj'; +// export const TEST_ACCOUNT_3_STX_ADDRESS = 'SP297VG59W96DPGBT13SGD542QE1XS954X78Z75G0' +export const TEST_ACCOUNT_3_PUBKEY = + '03c1e856462ca2844adb898aee90af5237e9d1be0fe51212635b2f7a643b0585e1'; export const TEST_BNS_NAME = 'test-hiro-wallet.btc'; export const TEST_BNS_RESOLVED_ADDRESS = 'SP12YQ0M2KFT7YMJKVGP71B874YF055F77PFPH9KM'; diff --git a/tests/specs/rpc-stacks-transaction/transaction-signing.spec.ts b/tests/specs/rpc-stacks-transaction/transaction-signing.spec.ts new file mode 100644 index 00000000000..cd2d765472c --- /dev/null +++ b/tests/specs/rpc-stacks-transaction/transaction-signing.spec.ts @@ -0,0 +1,130 @@ +import { BrowserContext, Page } from '@playwright/test'; +import { MultiSigSpendingCondition, deserializeTransaction } from '@stacks/transactions'; +import { TokenTransferPayload } from '@stacks/transactions/dist/payload'; +import { + TEST_ACCOUNT_1_PUBKEY, + TEST_ACCOUNT_2_STX_ADDRESS, + TEST_ACCOUNT_3_PUBKEY, +} from '@tests/mocks/constants'; +import { generateMultisigUnsignedStxTransfer, generateUnsignedStxTransfer } from '@tests/utils'; + +import { test } from '../../fixtures/fixtures'; + +test.describe('Transaction signing', () => { + test.beforeEach(async ({ extensionId, globalPage, onboardingPage, page }) => { + await globalPage.setupAndUseApiCalls(extensionId); + await onboardingPage.signInWithTestAccount(extensionId); + await page.goto('https://leather.io'); + }); + + function checkVisibleContent(context: BrowserContext) { + return async (buttonToPress: 'Cancel' | 'Confirm') => { + const popup = await context.waitForEvent('page'); + await popup.waitForSelector('text="' + TEST_ACCOUNT_2_STX_ADDRESS + '"'); + await popup.waitForSelector(`text="${500 * 0.000001}"`); + await popup.waitForTimeout(500); + const btn = popup.locator('text="Confirm"'); + + if (buttonToPress === 'Confirm') { + await btn.click(); + } else { + await popup.close(); + } + }; + } + + function initiateTxSigning(page: Page) { + return async (txHex: string) => + page.evaluate( + async txHex => + (window as any).HiroWalletProvider.request('stx_signTransaction', { + txHex, + network: 'mainnet', + }).catch((e: unknown) => e), + txHex + ); + } + + test('that transaction details are the same after signing multi-signature STX transfer', async ({ + page, + context, + }) => { + const amount = 500; + const multiSignatureTxHex = await generateMultisigUnsignedStxTransfer( + TEST_ACCOUNT_2_STX_ADDRESS, + amount, + 'mainnet', + [TEST_ACCOUNT_3_PUBKEY, TEST_ACCOUNT_1_PUBKEY], + 2, + 0 + ); + const [result] = await Promise.all([ + initiateTxSigning(page)(multiSignatureTxHex), + checkVisibleContent(context)('Confirm'), + ]); + + // deserialize both transactions + const deserializedUnsignedTxHex = deserializeTransaction(multiSignatureTxHex); + const deserializedSignedTx = deserializeTransaction(result.result.txHex); + // compare transactions + test + .expect((deserializedUnsignedTxHex.payload as TokenTransferPayload).recipient) + .toEqual((deserializedSignedTx.payload as TokenTransferPayload).recipient); + test + .expect((deserializedUnsignedTxHex.payload as TokenTransferPayload).amount) + .toEqual((deserializedSignedTx.payload as TokenTransferPayload).amount); + test.expect(deserializedUnsignedTxHex.payload.type).toEqual(deserializedSignedTx.payload.type); + test + .expect(deserializedUnsignedTxHex.auth.spendingCondition.nonce) + .toEqual(deserializedSignedTx.auth.spendingCondition.nonce); + test + .expect(deserializedUnsignedTxHex.auth.spendingCondition.fee) + .toEqual(deserializedSignedTx.auth.spendingCondition.fee); + test + .expect( + (deserializedUnsignedTxHex.auth.spendingCondition as MultiSigSpendingCondition) + .signaturesRequired + ) + .toEqual( + (deserializedSignedTx.auth.spendingCondition as MultiSigSpendingCondition) + .signaturesRequired + ); + test + .expect(deserializedUnsignedTxHex.auth.spendingCondition.signer) + .toEqual(deserializedSignedTx.auth.spendingCondition.signer); + test + .expect(deserializedUnsignedTxHex.auth.spendingCondition.hashMode) + .toEqual(deserializedSignedTx.auth.spendingCondition.hashMode); + // check that the transaction is signed + test + .expect( + (deserializedSignedTx.auth.spendingCondition as MultiSigSpendingCondition).fields.length + ) + .toEqual(1); + }); + + test('Single signature STX transfer being rejected', async ({ page, context }) => { + const amount = 500; + const singleSignatureTxHex = await generateUnsignedStxTransfer( + TEST_ACCOUNT_2_STX_ADDRESS, + amount, + 'mainnet', + TEST_ACCOUNT_3_PUBKEY + ); + const [result] = await Promise.all([ + initiateTxSigning(page)(singleSignatureTxHex), + checkVisibleContent(context)('Cancel'), + ]); + + // ID is random, removed so we can test known values + delete result.id; + + test.expect(result).toEqual({ + jsonrpc: '2.0', + error: { + code: 4001, + message: 'User rejected the Stacks transaction signing request', + }, + }); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts index 26fcb538a3e..4fd69f04390 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,4 +1,6 @@ import { Locator } from '@playwright/test'; +import { bytesToHex } from '@stacks/common'; +import { AnchorMode, makeUnsignedSTXTokenTransfer } from '@stacks/transactions'; import { SharedComponentsSelectors } from './selectors/shared-component.selectors'; @@ -20,3 +22,45 @@ export async function getDisplayerAddress(locator: Locator) { return displayerAddress.replaceAll('\n', ''); } + +export async function generateUnsignedStxTransfer( + recipient: string, + amount: number, + network: any, + publicKey: string, + anchorMode?: AnchorMode, + memo?: string +) { + const options = { + recipient, + memo, + publicKey, + anchorMode: anchorMode ?? AnchorMode.Any, + amount, + network, + }; + return bytesToHex((await makeUnsignedSTXTokenTransfer(options)).serialize()); +} + +export async function generateMultisigUnsignedStxTransfer( + recipient: string, + amount: number, + network: any, + publicKeys: string[], + threshold: number, + nonce: number, + anchorMode?: AnchorMode, + memo?: string +) { + const options = { + recipient, + memo, + publicKeys, + nonce, + numSignatures: threshold, + anchorMode: anchorMode ?? AnchorMode.Any, + amount, + network, + }; + return bytesToHex((await makeUnsignedSTXTokenTransfer(options)).serialize()); +} diff --git a/webpack/webpack.config.base.js b/webpack/webpack.config.base.js index c2554fe3bf9..35871201bc0 100755 --- a/webpack/webpack.config.base.js +++ b/webpack/webpack.config.base.js @@ -10,31 +10,40 @@ const CopyWebpackPlugin = require('copy-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -const SpeedMeasurePlugin = require('speed-measure-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const SRC_ROOT_PATH = path.join(__dirname, '../', 'src'); const DIST_ROOT_PATH = path.join(__dirname, '../', 'dist'); +const { execSync } = require('child_process'); -const WALLET_ENVIRONMENT = process.env.WALLET_ENVIRONMENT || 'development'; +const WALLET_ENVIRONMENT = process.env.WALLET_ENVIRONMENT ?? 'development'; const ANALYZE_BUNDLE = process.env.ANALYZE === 'true'; const IS_PUBLISHING = !!process.env.IS_PUBLISHING; -const BRANCH = process.env.GITHUB_REF; const IS_DEV = WALLET_ENVIRONMENT === 'development'; const IS_PROD = !IS_DEV; const MAIN_BRANCH = 'refs/heads/main'; +function executeGitCommand(command) { + return execSync(command) + .toString('utf8') + .replace(/[\n\r\s]+$/, ''); +} + +const BRANCH_NAME = executeGitCommand('git rev-parse --abbrev-ref HEAD'); +const COMMIT_SHA = process.env.COMMIT_SHA ?? executeGitCommand('git rev-parse HEAD'); + +console.log({ + BRANCH_NAME, + envSha: process.env.COMMIT_SHA, + locallyExe: executeGitCommand('git rev-parse HEAD'), +}); + // For non main branch builds, add a random number const getVersionWithRandomSuffix = ref => { if (ref === MAIN_BRANCH || !ref || IS_PUBLISHING) return _version; return `${_version}.${Math.floor(Math.floor(Math.random() * 1000))}`; }; -const VERSION = getVersionWithRandomSuffix(BRANCH); - -const smp = new SpeedMeasurePlugin({ - disable: !ANALYZE_BUNDLE, - granularLoaderData: true, -}); +const VERSION = getVersionWithRandomSuffix(BRANCH_NAME); const HTML_OPTIONS = { inject: 'body', @@ -247,6 +256,11 @@ const config = { new webpack.DefinePlugin({ VERSION: JSON.stringify(VERSION), }), + + new webpack.EnvironmentPlugin({ + BRANCH_NAME: BRANCH_NAME, + COMMIT_SHA: COMMIT_SHA, + }), new webpack.ProvidePlugin({ process: 'process/browser', Buffer: ['buffer', 'Buffer'], diff --git a/yarn.lock b/yarn.lock index f050a2d3ad6..576f89cb383 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2016,6 +2016,18 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@octokit/openapi-types@^19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-19.0.0.tgz#0101bf62ab14c1946149a0f8385440963e1253c4" + integrity sha512-PclQ6JGMTE9iUStpzMkwLCISFn/wDeRjkZFIKALpvJQNBGwDoYYi2fFvuHwssoQ1rXI5mfh6jgTgWuddeUzfWw== + +"@octokit/types@12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.0.0.tgz#6b34309288b6f5ac9761d2589e3165cde1b95fee" + integrity sha512-EzD434aHTFifGudYAygnFlS1Tl6KhbTynEWELQXIbTY8Msvb5nEqTZIm7sbPEt4mQYLZwu3zPKVdeIrw0g7ovg== + dependencies: + "@octokit/openapi-types" "^19.0.0" + "@pandacss/config@0.15.4", "@pandacss/config@^0.15.4": version "0.15.4" resolved "https://registry.yarnpkg.com/@pandacss/config/-/config-0.15.4.tgz#3300288a182e02d5f6e1f2907948b396f62b760f" @@ -10755,10 +10767,10 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -dependency-cruiser@14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-14.1.0.tgz#7312f17e905f6183e3e47298ed2019b20534b217" - integrity sha512-JF7F0SFG4K5vXmUMvgYHKQnMuU2JzO18/+r/hTuaGEr3KTlMYkR16WNc+WDqS0y5fjq8khDy/WKO4bR5xhw2sQ== +dependency-cruiser@14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/dependency-cruiser/-/dependency-cruiser-14.1.1.tgz#8d466ebe69af7c85af3670ba947c7b196d23260d" + integrity sha512-npNLWv11pMH9BW4GBLuA5p6KYOXA9UjVDKQ4DzorEhAac5BS1J23K5I2WpEfkJMpwl9PKMsF4T/GDLSq3pogTw== dependencies: acorn "8.10.0" acorn-jsx "5.3.2" @@ -10784,7 +10796,7 @@ dependency-cruiser@14.1.0: semver-try-require "6.2.3" teamcity-service-messages "0.1.14" tsconfig-paths-webpack-plugin "4.1.0" - watskeburt "1.0.1" + watskeburt "2.0.0" wrap-ansi "8.1.0" dequal@^2.0.0: @@ -20195,10 +20207,10 @@ watchpack@2.4.0, watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -watskeburt@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-1.0.1.tgz#157e87319cedac222c2524e138136fad70ba253e" - integrity sha512-MOvC8vf3hAVo1HPF/pkba7065mt6A/P9unLlFvYhZ7Yyuht16tmfCYi/LqHABG4hIRMZCbvY8eDWHPy81eSADA== +watskeburt@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watskeburt/-/watskeburt-2.0.0.tgz#9599978fdf3c994354390bac7ca368726b6771ac" + integrity sha512-RJ961Bcw9sfHr1NqZwvcFBYWo6bN9xE1CeBy6LigLqpzzrdnvsMT5HFg2JhOe4ioDOrCndjNa3tsErIVZtCc3g== wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3"