From 43dd138369a6bc69694be715b8858a129401497d Mon Sep 17 00:00:00 2001 From: kyranjamie Date: Wed, 20 Mar 2024 15:33:34 +0100 Subject: [PATCH] feat: compliance checks --- pnpm-lock.yaml | 3 - .../account/use-refresh-all-account-data.ts | 3 +- .../hooks/use-submit-stx-transaction.ts | 4 +- src/app/common/utils.ts | 29 +----- .../validation/forms/compliance-validators.ts | 29 ++++++ .../validation/forms/recipient-validators.ts | 10 +- .../loaders/bitcoin-account-loader.tsx | 19 ++-- src/app/components/no-fees-warning-row.tsx | 9 +- .../connect-ledger-asset-button.tsx | 1 - .../features/collectibles/collectibles.tsx | 8 +- .../leather-intro-dialog.tsx | 3 +- .../ledger-bitcoin-sign-tx-container.tsx | 2 +- .../jwt-signing/ledger-sign-jwt-container.tsx | 3 +- .../request-bitcoin-keys.utils.ts | 3 +- .../request-stacks-keys.utils.ts | 3 +- .../ledger-stacks-sign-msg-container.tsx | 3 +- .../message-signing.utils.ts | 9 +- .../ledger-sign-stacks-tx-container.tsx | 3 +- .../steps/contract-principal-bug-warning.tsx | 3 +- .../request-keys/use-request-ledger-keys.ts | 4 +- .../tx-signing/use-ledger-sign-tx.ts | 4 +- .../ledger/utils/generic-ledger-utils.ts | 3 +- ...e-parsed-psbt.tsx => use-psbt-details.tsx} | 4 +- src/app/features/psbt-signer/psbt-signer.tsx | 7 +- .../retrieve-taproot-to-native-segwit.tsx | 2 +- .../stacks-message-signing.utils.ts | 3 +- .../use-stacks-broadcast-transaction.tsx | 3 +- .../pages/home/components/account-actions.tsx | 2 +- .../onboarding/sign-in/hooks/use-sign-in.ts | 2 +- .../rpc-send-transfer/rpc-send-transfer.tsx | 3 + .../use-sign-bip322-message.ts | 3 +- .../rpc-sign-stacks-transaction.tsx | 4 + .../use-rpc-sign-stacks-transaction.ts | 5 +- .../hooks/use-send-inscription-form.tsx | 8 ++ .../form/btc/use-btc-send-form.tsx | 8 ++ .../swap/hooks/use-alex-broadcast-swap.ts | 2 +- .../use-bitcoin-broadcast-transaction.ts | 3 +- .../compliance-checker.query.ts | 77 ++++++++++++++++ src/app/query/stacks/hiro-rate-limiter.ts | 3 +- .../blockchain/stacks/stacks-accounts.ts | 3 +- src/app/store/common/api-clients.hooks.ts | 2 +- src/app/store/networks/networks.hooks.ts | 2 +- src/app/store/transactions/transaction.ts | 3 +- src/shared/constants.ts | 2 + .../crypto/stacks/stacks.utils.spec.ts} | 2 +- src/shared/crypto/stacks/stacks.utils.ts | 19 ++++ src/shared/utils.ts | 8 ++ .../compliance-checks.spec.ts | 92 +++++++++++++++++++ 48 files changed, 331 insertions(+), 99 deletions(-) create mode 100644 src/app/common/validation/forms/compliance-validators.ts rename src/app/features/psbt-signer/hooks/{use-parsed-psbt.tsx => use-psbt-details.tsx} (95%) create mode 100644 src/app/query/common/compliance-checker/compliance-checker.query.ts rename src/{app/common/transactions/stacks/transaction.utils.spec.ts => shared/crypto/stacks/stacks.utils.spec.ts} (93%) create mode 100644 tests/specs/compliance-checks/compliance-checks.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd6ce947970..4ed7f02cbad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21684,9 +21684,6 @@ packages: /sqlite3@5.1.6: resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} requiresBuild: true - peerDependenciesMeta: - node-gyp: - optional: true dependencies: '@mapbox/node-pre-gyp': 1.0.11 node-addon-api: 4.3.0 diff --git a/src/app/common/hooks/account/use-refresh-all-account-data.ts b/src/app/common/hooks/account/use-refresh-all-account-data.ts index e7adcb1921e..129bd31bc99 100644 --- a/src/app/common/hooks/account/use-refresh-all-account-data.ts +++ b/src/app/common/hooks/account/use-refresh-all-account-data.ts @@ -1,6 +1,7 @@ import { useCallback } from 'react'; -import { delay } from '@app/common/utils'; +import { delay } from '@shared/utils'; + import { useCurrentAccountMempool } from '@app/query/stacks/mempool/mempool.hooks'; // TODO: Can this be removed? It seems like we should be able diff --git a/src/app/common/hooks/use-submit-stx-transaction.ts b/src/app/common/hooks/use-submit-stx-transaction.ts index f8b9f72ea8f..d58997dcec5 100644 --- a/src/app/common/hooks/use-submit-stx-transaction.ts +++ b/src/app/common/hooks/use-submit-stx-transaction.ts @@ -4,7 +4,7 @@ import { bytesToHex } from '@stacks/common'; import { StacksTransaction, broadcastTransaction } from '@stacks/transactions'; import { logger } from '@shared/logger'; -import { isError } from '@shared/utils'; +import { delay, isError } from '@shared/utils'; import { getErrorMessage } from '@app/common/get-error-message'; import { useRefreshAllAccountData } from '@app/common/hooks/account/use-refresh-all-account-data'; @@ -15,8 +15,6 @@ import { useToast } from '@app/features/toasts/use-toast'; import { useCurrentStacksNetworkState } from '@app/store/networks/networks.hooks'; import { useSubmittedTransactionsActions } from '@app/store/submitted-transactions/submitted-transactions.hooks'; -import { delay } from '../utils'; - const timeForApiToUpdate = 250; interface UseSubmitTransactionArgs { diff --git a/src/app/common/utils.ts b/src/app/common/utils.ts index 400db14c612..df08b7bbef9 100644 --- a/src/app/common/utils.ts +++ b/src/app/common/utils.ts @@ -1,10 +1,5 @@ import { hexToBytes } from '@stacks/common'; -import { - BytesReader, - ChainID, - PostCondition, - deserializePostCondition, -} from '@stacks/transactions'; +import { BytesReader, PostCondition, deserializePostCondition } from '@stacks/transactions'; import { toUnicode } from 'punycode'; import { BitcoinChainConfig, BitcoinNetworkModes, KEBAB_REGEX } from '@shared/constants'; @@ -225,14 +220,6 @@ export function addPortSuffix(url: string) { return port ? `:${port}` : ''; } -export async function delay(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -export function createDelay(ms: number) { - return async () => delay(ms); -} - export function with0x(value: string): string { return !value.startsWith('0x') ? `0x${value}` : value; } @@ -249,9 +236,11 @@ export function doesBrowserSupportWebUsbApi() { return Boolean((navigator as any).usb); } -const isFullPage = document.location.pathname.startsWith('/index.html'); +function isFullPage() { + return document.location.pathname.startsWith('/index.html'); +} -const pageMode = isFullPage ? 'full' : 'popup'; +const pageMode = isFullPage() ? 'full' : 'popup'; type PageMode = 'popup' | 'full'; @@ -266,14 +255,6 @@ export function isPopupMode() { return pageMode === 'popup'; } -interface WhenStacksChainIdMap { - [ChainID.Mainnet]: T; - [ChainID.Testnet]: T; -} -export function whenStacksChainId(chainId: ChainID) { - return (chainIdMap: WhenStacksChainIdMap): T => chainIdMap[chainId]; -} - export const parseIfValidPunycode = (s: string) => { try { return toUnicode(s); diff --git a/src/app/common/validation/forms/compliance-validators.ts b/src/app/common/validation/forms/compliance-validators.ts new file mode 100644 index 00000000000..5f27505b375 --- /dev/null +++ b/src/app/common/validation/forms/compliance-validators.ts @@ -0,0 +1,29 @@ +import { isString } from 'formik'; +import * as yup from 'yup'; + +import type { NetworkModes } from '@shared/constants'; + +import { checkEntityAddressIsCompliant } from '@app/query/common/compliance-checker/compliance-checker.query'; + +export function complianceValidator( + shouldCheckCompliance: yup.StringSchema, + network: NetworkModes +) { + return yup.string().test({ + message: 'Compliance check failed', + async test(value) { + if (!isString(value)) return false; + + if (network !== 'mainnet') return true; + + if (!shouldCheckCompliance.isValidSync(value)) return true; + + try { + const resp = await checkEntityAddressIsCompliant(value); + return !resp.isOnSanctionsList; + } catch (e) { + return true; + } + }, + }); +} diff --git a/src/app/common/validation/forms/recipient-validators.ts b/src/app/common/validation/forms/recipient-validators.ts index d8f04b0e510..04f5074ce53 100644 --- a/src/app/common/validation/forms/recipient-validators.ts +++ b/src/app/common/validation/forms/recipient-validators.ts @@ -1,4 +1,5 @@ import { NetworkConfiguration } from '@shared/constants'; +import { stacksChainIdToCoreNetworkMode } from '@shared/crypto/stacks/stacks.utils'; import { FormErrorMessages } from '@app/common/error-messages'; @@ -7,6 +8,7 @@ import { stxAddressNetworkValidator, stxAddressValidator, } from './address-validators'; +import { complianceValidator } from './compliance-validators'; export function stxRecipientValidator( currentAddress: string, @@ -14,5 +16,11 @@ export function stxRecipientValidator( ) { return stxAddressValidator(FormErrorMessages.InvalidAddress) .concat(stxAddressNetworkValidator(currentNetwork)) - .concat(notCurrentAddressValidator(currentAddress || '')); + .concat(notCurrentAddressValidator(currentAddress || '')) + .concat( + complianceValidator( + stxAddressValidator(FormErrorMessages.InvalidAddress), + stacksChainIdToCoreNetworkMode(currentNetwork.chain.stacks.chainId) + ) + ); } diff --git a/src/app/components/loaders/bitcoin-account-loader.tsx b/src/app/components/loaders/bitcoin-account-loader.tsx index 48766ca5541..d97252f68a2 100644 --- a/src/app/components/loaders/bitcoin-account-loader.tsx +++ b/src/app/components/loaders/bitcoin-account-loader.tsx @@ -1,14 +1,17 @@ -import { BitcoinAccount } from '@shared/crypto/bitcoin/bitcoin.utils'; +import type { P2Ret, P2TROut } from '@scure/btc-signer'; -import { useCurrentNativeSegwitAccount } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; -import { useCurrentTaprootAccount } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; +import { ZERO_INDEX } from '@shared/constants'; -interface CurrentBitcoinAccountLoaderProps { - children(data: { nativeSegwit: BitcoinAccount; taproot: BitcoinAccount }): React.ReactNode; +import type { Signer } from '@app/store/accounts/blockchain/bitcoin/bitcoin-signer'; +import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCurrentAccountTaprootSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; + +interface CurrentBitcoinSignerLoaderProps { + children(data: { nativeSegwit: Signer; taproot: Signer }): React.ReactNode; } -export function CurrentBitcoinAccountLoader({ children }: CurrentBitcoinAccountLoaderProps) { - const nativeSegwit = useCurrentNativeSegwitAccount(); - const taproot = useCurrentTaprootAccount(); +export function CurrentBitcoinSignerLoader({ children }: CurrentBitcoinSignerLoaderProps) { + const nativeSegwit = useCurrentAccountNativeSegwitSigner()?.(ZERO_INDEX); + const taproot = useCurrentAccountTaprootSigner()?.(ZERO_INDEX); if (!taproot || !nativeSegwit) return null; return children({ nativeSegwit, taproot }); } diff --git a/src/app/components/no-fees-warning-row.tsx b/src/app/components/no-fees-warning-row.tsx index 420bf92a6d5..aea63ef01bf 100644 --- a/src/app/components/no-fees-warning-row.tsx +++ b/src/app/components/no-fees-warning-row.tsx @@ -1,7 +1,9 @@ import { ChainID } from '@stacks/transactions'; import { HStack, styled } from 'leather-styles/jsx'; -import { whenStacksChainId } from '@app/common/utils'; +import { stacksChainIdToCoreNetworkMode } from '@shared/crypto/stacks/stacks.utils'; + +import { capitalize } from '@app/common/utils'; interface NoFeesWarningRowProps { chainId: ChainID; @@ -11,10 +13,7 @@ export function NoFeesWarningRow({ chainId }: NoFeesWarningRowProps) { No fees are incurred - {whenStacksChainId(chainId)({ - [ChainID.Testnet]: 'Testnet', - [ChainID.Mainnet]: 'Mainnet', - })} + {capitalize(stacksChainIdToCoreNetworkMode(chainId))} ); diff --git a/src/app/features/asset-list/components/connect-ledger-asset-button.tsx b/src/app/features/asset-list/components/connect-ledger-asset-button.tsx index 3e8d7bec072..bc08367fa05 100644 --- a/src/app/features/asset-list/components/connect-ledger-asset-button.tsx +++ b/src/app/features/asset-list/components/connect-ledger-asset-button.tsx @@ -13,7 +13,6 @@ import { LedgerIcon } from '@app/ui/icons/ledger-icon'; interface ConnectLedgerAssetBtnProps { chain: SupportedBlockchains; } - export function ConnectLedgerAssetBtn({ chain }: ConnectLedgerAssetBtnProps) { const navigate = useNavigate(); diff --git a/src/app/features/collectibles/collectibles.tsx b/src/app/features/collectibles/collectibles.tsx index 2cb61a05127..4a134de63c1 100644 --- a/src/app/features/collectibles/collectibles.tsx +++ b/src/app/features/collectibles/collectibles.tsx @@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { RouteUrls } from '@shared/route-urls'; import { useWalletType } from '@app/common/use-wallet-type'; -import { CurrentBitcoinAccountLoader } from '@app/components/loaders/bitcoin-account-loader'; +import { CurrentBitcoinSignerLoader } from '@app/components/loaders/bitcoin-account-loader'; import { CurrentStacksAccountLoader } from '@app/components/loaders/stacks-account-loader'; import { useConfigNftMetadataEnabled } from '@app/query/common/remote-config/remote-config.query'; @@ -47,7 +47,7 @@ export function Collectibles() { isLoadingMore={isLoadingMore} onRefresh={() => void queryClient.refetchQueries({ type: 'active' })} > - {() => } + {() => } {isNftMetadataEnabled && ( @@ -55,14 +55,14 @@ export function Collectibles() { )} - + {() => ( <> )} - + ); } diff --git a/src/app/features/dialogs/leather-intro-dialog/leather-intro-dialog.tsx b/src/app/features/dialogs/leather-intro-dialog/leather-intro-dialog.tsx index dc59d5f6e00..c30778281f4 100644 --- a/src/app/features/dialogs/leather-intro-dialog/leather-intro-dialog.tsx +++ b/src/app/features/dialogs/leather-intro-dialog/leather-intro-dialog.tsx @@ -1,8 +1,9 @@ import { createContext, useContext } from 'react'; import { Outlet, Route, useNavigate } from 'react-router-dom'; +import { delay } from '@shared/utils'; + import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; -import { delay } from '@app/common/utils'; import { openInNewTab } from '@app/common/utils/open-in-new-tab'; import { diff --git a/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx b/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx index a48b8bb90d5..84e04f45a15 100644 --- a/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx +++ b/src/app/features/ledger/flows/bitcoin-tx-signing/ledger-bitcoin-sign-tx-container.tsx @@ -9,11 +9,11 @@ import get from 'lodash.get'; import { BitcoinInputSigningConfig } from '@shared/crypto/bitcoin/signer-config'; import { logger } from '@shared/logger'; import { RouteUrls } from '@shared/route-urls'; +import { delay } from '@shared/utils'; import { useLocationStateWithCache } from '@app/common/hooks/use-location-state'; import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; import { appEvents } from '@app/common/publish-subscribe'; -import { delay } from '@app/common/utils'; import { ApproveSignLedgerBitcoinTx } from '@app/features/ledger/flows/bitcoin-tx-signing/steps/approve-bitcoin-sign-ledger-tx'; import { ledgerSignTxRoutes } from '@app/features/ledger/generic-flows/tx-signing/ledger-sign-tx-route-generator'; import { LedgerTxSigningContext } from '@app/features/ledger/generic-flows/tx-signing/ledger-sign-tx.context'; diff --git a/src/app/features/ledger/flows/jwt-signing/ledger-sign-jwt-container.tsx b/src/app/features/ledger/flows/jwt-signing/ledger-sign-jwt-container.tsx index d28a93ce2b7..b50dc940b29 100644 --- a/src/app/features/ledger/flows/jwt-signing/ledger-sign-jwt-container.tsx +++ b/src/app/features/ledger/flows/jwt-signing/ledger-sign-jwt-container.tsx @@ -7,7 +7,7 @@ import get from 'lodash.get'; import { finalizeAuthResponse } from '@shared/actions/finalize-auth-response'; import { logger } from '@shared/logger'; -import { isError } from '@shared/utils'; +import { delay, isError } from '@shared/utils'; import { useGetLegacyAuthBitcoinAddresses } from '@app/common/authentication/use-legacy-auth-bitcoin-addresses'; import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state'; @@ -15,7 +15,6 @@ import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-s import { useKeyActions } from '@app/common/hooks/use-key-actions'; import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; import { makeLedgerCompatibleUnsignedAuthResponsePayload } from '@app/common/unsafe-auth-response'; -import { delay } from '@app/common/utils'; import { getStacksAppVersion, prepareLedgerDeviceStacksAppConnection, diff --git a/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts b/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts index 2198f5ec7c4..a54c6adc53f 100644 --- a/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts +++ b/src/app/features/ledger/flows/request-bitcoin-keys/request-bitcoin-keys.utils.ts @@ -4,8 +4,7 @@ import { BitcoinNetworkModes, NetworkModes } from '@shared/constants'; import { createWalletIdDecoratedPath } from '@shared/crypto/bitcoin/bitcoin.utils'; import { getTaprootAccountDerivationPath } from '@shared/crypto/bitcoin/p2tr-address-gen'; import { getNativeSegwitAccountDerivationPath } from '@shared/crypto/bitcoin/p2wpkh-address-gen'; - -import { delay } from '@app/common/utils'; +import { delay } from '@shared/utils'; import { WalletPolicyDetails, diff --git a/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts b/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts index 317d59da44b..76b4a021991 100644 --- a/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts +++ b/src/app/features/ledger/flows/request-stacks-keys/request-stacks-keys.utils.ts @@ -6,8 +6,7 @@ import { getIdentityDerivationPath, getStxDerivationPath, } from '@shared/crypto/stacks/stacks.utils'; - -import { delay } from '@app/common/utils'; +import { delay } from '@shared/utils'; import { StacksAppKeysResponseItem, diff --git a/src/app/features/ledger/flows/stacks-message-signing/ledger-stacks-sign-msg-container.tsx b/src/app/features/ledger/flows/stacks-message-signing/ledger-stacks-sign-msg-container.tsx index 6554b51c2d2..4ef7eaf1d46 100644 --- a/src/app/features/ledger/flows/stacks-message-signing/ledger-stacks-sign-msg-container.tsx +++ b/src/app/features/ledger/flows/stacks-message-signing/ledger-stacks-sign-msg-container.tsx @@ -7,11 +7,10 @@ import { LedgerError } from '@zondax/ledger-stacks'; import get from 'lodash.get'; import { UnsignedMessage, whenSignableMessageOfType } from '@shared/signature/signature-types'; -import { isError } from '@shared/utils'; +import { delay, isError } from '@shared/utils'; import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; import { appEvents } from '@app/common/publish-subscribe'; -import { delay } from '@app/common/utils'; import { getStacksAppVersion, prepareLedgerDeviceStacksAppConnection, diff --git a/src/app/features/ledger/flows/stacks-message-signing/message-signing.utils.ts b/src/app/features/ledger/flows/stacks-message-signing/message-signing.utils.ts index ab74689469f..e3ca27f5976 100644 --- a/src/app/features/ledger/flows/stacks-message-signing/message-signing.utils.ts +++ b/src/app/features/ledger/flows/stacks-message-signing/message-signing.utils.ts @@ -8,9 +8,10 @@ import { encodeStructuredData, } from '@stacks/transactions'; +import { stacksChainIdToCoreNetworkMode } from '@shared/crypto/stacks/stacks.utils'; import { UnsignedMessageStructured } from '@shared/signature/signature-types'; -import { whenStacksChainId } from '@app/common/utils'; +import { capitalize } from '@app/common/utils'; export function cvToDisplay(cv: ClarityValue): string { return cvToString(cv).replaceAll('"', ''); @@ -21,11 +22,7 @@ export function chainIdToDisplay(chainIdCv: ClarityValue): string { const chainIdString = cvToString(chainIdCv); const chainId = parseInt(chainIdString.replace('u', '')); if (!Object.values(ChainID).includes(chainId)) return ''; - - return whenStacksChainId(chainId as ChainID)({ - [ChainID.Testnet]: 'Testnet', - [ChainID.Mainnet]: 'Mainnet', - }); + return capitalize(stacksChainIdToCoreNetworkMode(chainId)); } export function deriveStructuredMessageHash({ diff --git a/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-stacks-tx-container.tsx b/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-stacks-tx-container.tsx index 455f2e97640..fb9d9b7396b 100644 --- a/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-stacks-tx-container.tsx +++ b/src/app/features/ledger/flows/stacks-tx-signing/ledger-sign-stacks-tx-container.tsx @@ -6,11 +6,10 @@ import StacksApp, { LedgerError } from '@zondax/ledger-stacks'; import get from 'lodash.get'; import { RouteUrls } from '@shared/route-urls'; -import { isError } from '@shared/utils'; +import { delay, isError } from '@shared/utils'; import { useScrollLock } from '@app/common/hooks/use-scroll-lock'; import { appEvents } from '@app/common/publish-subscribe'; -import { delay } from '@app/common/utils'; import { LedgerTxSigningContext } from '@app/features/ledger/generic-flows/tx-signing/ledger-sign-tx.context'; import { connectLedgerStacksApp, diff --git a/src/app/features/ledger/flows/stacks-tx-signing/steps/contract-principal-bug-warning.tsx b/src/app/features/ledger/flows/stacks-tx-signing/steps/contract-principal-bug-warning.tsx index 1800cbb0a87..4f7c03af108 100644 --- a/src/app/features/ledger/flows/stacks-tx-signing/steps/contract-principal-bug-warning.tsx +++ b/src/app/features/ledger/flows/stacks-tx-signing/steps/contract-principal-bug-warning.tsx @@ -1,8 +1,9 @@ import GenericErrorImg from '@assets/images/generic-error.png'; import { Box, HStack, styled } from 'leather-styles/jsx'; +import { delay } from '@shared/utils'; + import { useLoading } from '@app/common/hooks/use-loading'; -import { delay } from '@app/common/utils'; import { LedgerTitle } from '@app/features/ledger/components/ledger-title'; import { LedgerWrapper } from '@app/features/ledger/components/ledger-wrapper'; import { Button } from '@app/ui/components/button/button'; diff --git a/src/app/features/ledger/generic-flows/request-keys/use-request-ledger-keys.ts b/src/app/features/ledger/generic-flows/request-keys/use-request-ledger-keys.ts index d83a9afa5ce..f1f854542ad 100644 --- a/src/app/features/ledger/generic-flows/request-keys/use-request-ledger-keys.ts +++ b/src/app/features/ledger/generic-flows/request-keys/use-request-ledger-keys.ts @@ -4,9 +4,7 @@ import StacksApp from '@zondax/ledger-stacks'; import BitcoinApp from 'ledger-bitcoin'; import { SupportedBlockchains } from '@shared/constants'; -import { isError } from '@shared/utils'; - -import { delay } from '@app/common/utils'; +import { delay, isError } from '@shared/utils'; import { useLedgerAnalytics } from '../../hooks/use-ledger-analytics.hook'; import { useLedgerNavigate } from '../../hooks/use-ledger-navigate'; diff --git a/src/app/features/ledger/generic-flows/tx-signing/use-ledger-sign-tx.ts b/src/app/features/ledger/generic-flows/tx-signing/use-ledger-sign-tx.ts index 49f980c46f6..d2fe32957dc 100644 --- a/src/app/features/ledger/generic-flows/tx-signing/use-ledger-sign-tx.ts +++ b/src/app/features/ledger/generic-flows/tx-signing/use-ledger-sign-tx.ts @@ -4,9 +4,7 @@ import StacksApp from '@zondax/ledger-stacks'; import BitcoinApp from 'ledger-bitcoin'; import { SupportedBlockchains } from '@shared/constants'; -import { isError } from '@shared/utils'; - -import { delay } from '@app/common/utils'; +import { delay, isError } from '@shared/utils'; import { useLedgerNavigate } from '../../hooks/use-ledger-navigate'; import { BitcoinAppVersion } from '../../utils/bitcoin-ledger-utils'; diff --git a/src/app/features/ledger/utils/generic-ledger-utils.ts b/src/app/features/ledger/utils/generic-ledger-utils.ts index ea22c6fff44..33504c50a4d 100644 --- a/src/app/features/ledger/utils/generic-ledger-utils.ts +++ b/src/app/features/ledger/utils/generic-ledger-utils.ts @@ -3,7 +3,8 @@ import { useState } from 'react'; import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; import BitcoinApp from 'ledger-bitcoin'; -import { delay } from '@app/common/utils'; +import { delay } from '@shared/utils'; + import { safeAwait } from '@app/common/utils/safe-await'; import { getStacksAppVersion } from './stacks-ledger-utils'; diff --git a/src/app/features/psbt-signer/hooks/use-parsed-psbt.tsx b/src/app/features/psbt-signer/hooks/use-psbt-details.tsx similarity index 95% rename from src/app/features/psbt-signer/hooks/use-parsed-psbt.tsx rename to src/app/features/psbt-signer/hooks/use-psbt-details.tsx index 81114d13347..a7ab5154ed3 100644 --- a/src/app/features/psbt-signer/hooks/use-parsed-psbt.tsx +++ b/src/app/features/psbt-signer/hooks/use-psbt-details.tsx @@ -14,12 +14,12 @@ import { useParsedOutputs } from './use-parsed-outputs'; import { usePsbtInscriptions } from './use-psbt-inscriptions'; import { usePsbtTotals } from './use-psbt-totals'; -interface UseParsedPsbtArgs { +interface UsePsbtDetailsArgs { inputs: btc.TransactionInput[]; indexesToSign?: number[]; outputs: btc.TransactionOutput[]; } -export function useParsedPsbt({ inputs, indexesToSign, outputs }: UseParsedPsbtArgs) { +export function usePsbtDetails({ inputs, indexesToSign, outputs }: UsePsbtDetailsArgs) { const network = useCurrentNetwork(); const bitcoinAddressNativeSegwit = useCurrentAccountNativeSegwitIndexZeroSigner().address; const { address: bitcoinAddressTaproot } = useCurrentAccountTaprootIndexZeroSigner(); diff --git a/src/app/features/psbt-signer/psbt-signer.tsx b/src/app/features/psbt-signer/psbt-signer.tsx index 1a719fa80be..9e7f0b3b42b 100644 --- a/src/app/features/psbt-signer/psbt-signer.tsx +++ b/src/app/features/psbt-signer/psbt-signer.tsx @@ -8,6 +8,7 @@ import { RouteUrls } from '@shared/route-urls'; import { closeWindow, isError } from '@shared/utils'; import { SignPsbtArgs } from '@app/common/psbt/requests'; +import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query'; import { useOnOriginTabClose } from '@app/routes/hooks/use-on-tab-closed'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentAccountTaprootIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/taproot-account.hooks'; @@ -17,7 +18,7 @@ import { Card } from '@app/ui/layout/card/card'; import { CardContent } from '@app/ui/layout/card/card-content'; import * as Psbt from './components'; -import { useParsedPsbt } from './hooks/use-parsed-psbt'; +import { usePsbtDetails } from './hooks/use-psbt-details'; import { usePsbtSigner } from './hooks/use-psbt-signer'; import { PsbtSignerContext, PsbtSignerProvider } from './psbt-signer.context'; @@ -64,12 +65,14 @@ export function PsbtSigner(props: PsbtSignerProps) { psbtInputs, psbtOutputs, shouldDefaultToAdvancedView, - } = useParsedPsbt({ + } = usePsbtDetails({ inputs: psbtTxInputs, indexesToSign, outputs: psbtTxOutputs, }); + useBreakOnNonCompliantEntity(psbtOutputs.map(output => output.address)); + const psbtSignerContext: PsbtSignerContext = { accountInscriptionsBeingTransferred, accountInscriptionsBeingReceived, diff --git a/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx b/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx index 2d0b0e5a7be..fb97771f79d 100644 --- a/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx +++ b/src/app/features/retrieve-taproot-to-native-segwit/retrieve-taproot-to-native-segwit.tsx @@ -3,10 +3,10 @@ import { useNavigate } from 'react-router-dom'; import { Stack } from 'leather-styles/jsx'; import { RouteUrls } from '@shared/route-urls'; +import { delay } from '@shared/utils'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { formatMoneyPadded } from '@app/common/money/format-money'; -import { delay } from '@app/common/utils'; import { FormAddressDisplayer } from '@app/components/address-displayer/form-address-displayer'; import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-card/info-card'; import { useToast } from '@app/features/toasts/use-toast'; diff --git a/src/app/features/stacks-message-signer/stacks-message-signing.utils.ts b/src/app/features/stacks-message-signer/stacks-message-signing.utils.ts index f08152512cb..d99cf5902d6 100644 --- a/src/app/features/stacks-message-signer/stacks-message-signing.utils.ts +++ b/src/app/features/stacks-message-signer/stacks-message-signing.utils.ts @@ -3,9 +3,8 @@ import { useCallback } from 'react'; import { ClarityValue, TupleCV, createStacksPrivateKey } from '@stacks/transactions'; import { signMessage, signStructuredDataMessage } from '@shared/crypto/sign-message'; -import { isString } from '@shared/utils'; +import { createDelay, isString } from '@shared/utils'; -import { createDelay } from '@app/common/utils'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; export const improveUxWithShortDelayAsStacksSigningIsSoFast = createDelay(1000); diff --git a/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx b/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx index 5c8b8669990..b3683f029b5 100644 --- a/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx +++ b/src/app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction.tsx @@ -7,13 +7,12 @@ import { finalizeTxSignature } from '@shared/actions/finalize-tx-signature'; import { logger } from '@shared/logger'; import { CryptoCurrencies } from '@shared/models/currencies.model'; import { RouteUrls } from '@shared/route-urls'; -import { isError, isString } from '@shared/utils'; +import { delay, isError, isString } from '@shared/utils'; import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; import { LoadingKeys } from '@app/common/hooks/use-loading'; import { useSubmitTransactionCallback } from '@app/common/hooks/use-submit-stx-transaction'; import { stacksTransactionToHex } from '@app/common/transactions/stacks/transaction.utils'; -import { delay } from '@app/common/utils'; import { useToast } from '@app/features/toasts/use-toast'; import { useTransactionRequest } from '@app/store/transactions/requests.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; diff --git a/src/app/pages/home/components/account-actions.tsx b/src/app/pages/home/components/account-actions.tsx index 14643d65c47..e672502ca1b 100644 --- a/src/app/pages/home/components/account-actions.tsx +++ b/src/app/pages/home/components/account-actions.tsx @@ -4,9 +4,9 @@ import { ChainID } from '@stacks/transactions'; import { HomePageSelectors } from '@tests/selectors/home.selectors'; import { Flex } from 'leather-styles/jsx'; +import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; import { RouteUrls } from '@shared/route-urls'; -import { whenStacksChainId } from '@app/common/utils'; import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query'; import { useCurrentAccountNativeSegwitIndexZeroSignerNullable } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; diff --git a/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts b/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts index ad2327be68f..0d5bc5fa458 100644 --- a/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts +++ b/src/app/pages/onboarding/sign-in/hooks/use-sign-in.ts @@ -5,10 +5,10 @@ import { validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { RouteUrls } from '@shared/route-urls'; +import { delay } from '@shared/utils'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useLoading } from '@app/common/hooks/use-loading'; -import { delay } from '@app/common/utils'; import { useAppDispatch } from '@app/store'; import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions'; import { useSeedInputErrorState } from '@app/store/onboarding/onboarding.hooks'; diff --git a/src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx b/src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx index 8809e1de7a4..22200e46073 100644 --- a/src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx +++ b/src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx @@ -4,6 +4,7 @@ import { createMoney } from '@shared/models/money.model'; import { formatMoneyPadded } from '@app/common/money/format-money'; import { InfoCardFooter } from '@app/components/info-card/info-card'; +import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query'; import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { Button } from '@app/ui/components/button/button'; @@ -17,6 +18,8 @@ export function RpcSendTransfer() { const amountAsMoney = createMoney(new BigNumber(amount), 'BTC'); const formattedMoney = formatMoneyPadded(amountAsMoney); + useBreakOnNonCompliantEntity(address); + return ( <> diff --git a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts index 7db34e2510a..ca5282bb8bf 100644 --- a/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts +++ b/src/app/pages/rpc-sign-bip322-message/use-sign-bip322-message.ts @@ -7,12 +7,11 @@ import * as bitcoin from 'bitcoinjs-lib'; import { signBip322MessageSimple } from '@shared/crypto/bitcoin/bip322/sign-message-bip322-bitcoinjs'; import { logger } from '@shared/logger'; import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods'; -import { closeWindow } from '@shared/utils'; +import { closeWindow, createDelay } from '@shared/utils'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; import { initialSearchParams } from '@app/common/initial-search-params'; -import { createDelay } from '@app/common/utils'; import { useToast } from '@app/features/toasts/use-toast'; import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; import { 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 index ef424ee3532..551cfbe397e 100644 --- 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 @@ -1,5 +1,6 @@ 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'; +import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query'; export function RpcSignStacksTransaction() { const { @@ -9,8 +10,11 @@ export function RpcSignStacksTransaction() { stacksTransaction, disableNonceSelection, isMultisig, + txSender, } = useRpcSignStacksTransaction(); + useBreakOnNonCompliantEntity(txSender); + return ( { + await registerEntityAddressComplianceCheck(address); + const entityReport = await checkEntityAddressComplianceCheck(address); + return { ...entityReport, isOnSanctionsList: entityReport.risk === 'Severe' }; +} + +const oneWeekInMs = 604_800_000; + +function makeComplianceQuery( + address: string, + network: BitcoinNetworkModes +): UseQueryOptions { + return { + enabled: network === 'mainnet', + queryKey: ['address-compliance-check', address], + async queryFn() { + return checkEntityAddressIsCompliant(address); + }, + cacheTime: Infinity, + staleTime: oneWeekInMs, + refetchInterval: oneWeekInMs, + refetchOnMount: false, + refetchOnReconnect: false, + refetchOnWindowFocus: false, + }; +} + +function useCheckAddressComplianceQueries(addresses: string[]) { + const network = useCurrentNetwork(); + return useQueries({ + queries: addresses.map(address => + makeComplianceQuery(address, network.chain.bitcoin.bitcoinNetwork) + ), + }); +} + +export function useBreakOnNonCompliantEntity(address: string | string[]) { + const analytics = useAnalytics(); + const complianceReports = useCheckAddressComplianceQueries(ensureArray(address)); + + if (complianceReports.some(report => report.data?.isOnSanctionsList)) { + void analytics.track('non_compliant_entity_detected'); + throw new Error('Unable to handle request, errorCode: 1398'); + } +} diff --git a/src/app/query/stacks/hiro-rate-limiter.ts b/src/app/query/stacks/hiro-rate-limiter.ts index 57cbfc94c90..97326b46708 100644 --- a/src/app/query/stacks/hiro-rate-limiter.ts +++ b/src/app/query/stacks/hiro-rate-limiter.ts @@ -1,7 +1,8 @@ import { ChainID } from '@stacks/transactions'; import PQueue from 'p-queue'; -import { whenStacksChainId } from '@app/common/utils'; +import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; + import { useCurrentNetworkState } from '@app/store/networks/networks.hooks'; const hiroStacksMainnetApiLimiter = new PQueue({ diff --git a/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts b/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts index 13cbd2e7e66..e30723e9fab 100644 --- a/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts +++ b/src/app/store/accounts/blockchain/stacks/stacks-accounts.ts @@ -14,10 +14,11 @@ import { deriveStxPrivateKey, generateWallet } from '@stacks/wallet-sdk'; import { atom } from 'jotai'; import { DATA_DERIVATION_PATH, deriveStacksSalt } from '@shared/crypto/stacks/stacks-address-gen'; +import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; import { defaultWalletKeyId } from '@shared/utils'; import { derivePublicKey } from '@app/common/keychain/keychain'; -import { createNullArrayOfLength, whenStacksChainId } from '@app/common/utils'; +import { createNullArrayOfLength } from '@app/common/utils'; import { storeAtom } from '@app/store'; import { selectStacksChain } from '@app/store/chains/stx-chain.selectors'; import { diff --git a/src/app/store/common/api-clients.hooks.ts b/src/app/store/common/api-clients.hooks.ts index 383095baea8..8103caf66cd 100644 --- a/src/app/store/common/api-clients.hooks.ts +++ b/src/app/store/common/api-clients.hooks.ts @@ -3,8 +3,8 @@ import { useMemo } from 'react'; import { ChainID } from '@stacks/transactions'; import { HIRO_API_BASE_URL_MAINNET, HIRO_API_BASE_URL_TESTNET } from '@shared/constants'; +import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; -import { whenStacksChainId } from '@app/common/utils'; import { BitcoinClient } from '@app/query/bitcoin/bitcoin-client'; import { StacksClient } from '@app/query/stacks/stacks-client'; import { TokenMetadataClient } from '@app/query/stacks/token-metadata-client'; diff --git a/src/app/store/networks/networks.hooks.ts b/src/app/store/networks/networks.hooks.ts index 0b962b54fea..4e7ce92e561 100644 --- a/src/app/store/networks/networks.hooks.ts +++ b/src/app/store/networks/networks.hooks.ts @@ -5,8 +5,8 @@ import { StacksNetwork } from '@stacks/network'; import { ChainID, TransactionVersion } from '@stacks/transactions'; import { NetworkModes } from '@shared/constants'; +import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; -import { whenStacksChainId } from '@app/common/utils'; import { useAppDispatch } from '@app/store'; import { networksActions } from './networks.actions'; diff --git a/src/app/store/transactions/transaction.ts b/src/app/store/transactions/transaction.ts index 7f93e5040aa..2dec507438a 100644 --- a/src/app/store/transactions/transaction.ts +++ b/src/app/store/transactions/transaction.ts @@ -1,7 +1,8 @@ import { ChainID, TransactionVersion } from '@stacks/transactions'; import { atom } from 'jotai'; -import { whenStacksChainId } from '@app/common/utils'; +import { whenStacksChainId } from '@shared/crypto/stacks/stacks.utils'; + import { currentNetworkAtom } from '@app/store/networks/networks'; export const transactionNetworkVersionState = atom(get => { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 20124ed5030..c6114a1f327 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -15,6 +15,8 @@ export const PERSISTENCE_CACHE_TIME = 1000 * 60 * 60 * 12; // 12 hours export const BTC_DECIMALS = 8; export const STX_DECIMALS = 6; +export const ZERO_INDEX = 0; + // https://bitcoin.stackexchange.com/a/41082/139277 export const BTC_P2WPKH_DUST_AMOUNT = 294; diff --git a/src/app/common/transactions/stacks/transaction.utils.spec.ts b/src/shared/crypto/stacks/stacks.utils.spec.ts similarity index 93% rename from src/app/common/transactions/stacks/transaction.utils.spec.ts rename to src/shared/crypto/stacks/stacks.utils.spec.ts index 16188824fbf..0413026735d 100644 --- a/src/app/common/transactions/stacks/transaction.utils.spec.ts +++ b/src/shared/crypto/stacks/stacks.utils.spec.ts @@ -1,6 +1,6 @@ import { ChainID } from '@stacks/transactions'; -import { whenStacksChainId } from '@app/common/utils'; +import { whenStacksChainId } from './stacks.utils'; describe(whenStacksChainId.name, () => { const expectedResult = 'should be this value'; diff --git a/src/shared/crypto/stacks/stacks.utils.ts b/src/shared/crypto/stacks/stacks.utils.ts index b8bea027d17..be6aa2e0265 100644 --- a/src/shared/crypto/stacks/stacks.utils.ts +++ b/src/shared/crypto/stacks/stacks.utils.ts @@ -1,3 +1,7 @@ +import { ChainID } from '@stacks/transactions'; + +import type { NetworkModes } from '@shared/constants'; + export const stxDerivationWithAccount = `m/44'/5757'/0'/0/{account}`; const stxIdentityDerivationWithAccount = `m/888'/0'/{account}'`; @@ -11,3 +15,18 @@ export const getStxDerivationPath = export const getIdentityDerivationPath = getAccountIndexFromDerivationPathFactory( stxIdentityDerivationWithAccount ); + +export function stacksChainIdToCoreNetworkMode(chainId: ChainID): NetworkModes { + return whenStacksChainId(chainId)({ + [ChainID.Mainnet]: 'mainnet', + [ChainID.Testnet]: 'testnet', + }); +} + +interface WhenStacksChainIdMap { + [ChainID.Mainnet]: T; + [ChainID.Testnet]: T; +} +export function whenStacksChainId(chainId: ChainID) { + return (chainIdMap: WhenStacksChainIdMap): T => chainIdMap[chainId]; +} diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 9605679b59e..64a143b5247 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -89,3 +89,11 @@ export function reverseBytes(bytes: Buffer | Uint8Array) { export function makeNumberRange(num: number) { return [...Array(num).keys()]; } + +export async function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export function createDelay(ms: number) { + return async () => delay(ms); +} diff --git a/tests/specs/compliance-checks/compliance-checks.spec.ts b/tests/specs/compliance-checks/compliance-checks.spec.ts new file mode 100644 index 00000000000..71d5722fac0 --- /dev/null +++ b/tests/specs/compliance-checks/compliance-checks.spec.ts @@ -0,0 +1,92 @@ +import type { BrowserContext, Page, Route } from '@playwright/test'; + +import { delay } from '@shared/utils'; + +import { test } from '../../fixtures/fixtures'; + +function mockChainalysisEntityRegistrationRequest(context: BrowserContext) { + return async (routeHandler: (route: Route) => void) => + context.route('https://api.chainalysis.com/api/risk/v2/entities', async route => + routeHandler(route) + ); +} + +function mockChainalysisEntityCheckRequest(context: BrowserContext) { + return async (routeHandler: (route: Route) => void) => + context.route('https://api.chainalysis.com/api/risk/v2/entities/*', async route => + routeHandler(route) + ); +} + +test.describe('Compliance checks', () => { + test.beforeEach(async ({ extensionId, globalPage, onboardingPage, page }) => { + await globalPage.setupAndUseApiCalls(extensionId); + await onboardingPage.signInWithTestAccount(extensionId); + await page.goto('localhost:3000', { waitUntil: 'networkidle' }); + }); + + async function openIllegalTransfer(page: Page) { + return page.evaluate( + () => + // We only want the page window, don't wait for actual promise to finish + void (window as any).LeatherProvider.request('sendTransfer', { + // Known address from list, in readme of this + // page https://github.com/0xB10C/ofac-sanctioned-digital-currency-addresses + address: '12QtD5BFwRsdNsAZY76UVE1xyCGNTojH9h', + amount: '1231', + }).catch((e: unknown) => e) + ); + } + + test('that it errors if non-compliant entity is detected', async ({ page, context }) => { + await mockChainalysisEntityRegistrationRequest(context)(route => + route.fulfill({ json: { address: '12QtD5BFwRsdNsAZY76UVE1xyCGNTojH9h' } }) + ); + await mockChainalysisEntityCheckRequest(context)(route => + route.fulfill({ json: { risk: 'Severe' } }) + ); + + const [leatherApprover] = await Promise.all([ + context.waitForEvent('page'), + openIllegalTransfer(page), + ]); + + await test + .expect(leatherApprover.locator('text="Unable to handle request, errorCode: 1398"')) + .toBeVisible(); + }); + + test('nothing happens when chainalysis is down', async ({ page, context }) => { + await mockChainalysisEntityCheckRequest(context)(route => route.abort()); + await mockChainalysisEntityRegistrationRequest(context)(route => route.abort()); + + const [leatherApprover] = await Promise.all([ + context.waitForEvent('page'), + openIllegalTransfer(page), + ]); + + await test + .expect(leatherApprover.locator('text="Unable to handle request, errorCode: 1398"')) + .toBeHidden(); + + await test.expect(leatherApprover.locator('text="0.00001231 BTC"')).toBeVisible(); + }); + + test('the addresses of all recipients are checked', async ({ context, page }) => { + let entityCheckCount = 0; + + await mockChainalysisEntityCheckRequest(context)(route => { + entityCheckCount += 1; + return route.abort(); + }); + + await Promise.all([context.waitForEvent('page'), openIllegalTransfer(page)]); + + // Please forgive this timeout, we need to give the page time in order to + // make the request, to be sure it was made. If this test ends up failing + // due to a race condition, please let the author know. + await delay(500); + + test.expect(entityCheckCount).toEqual(1); + }); +});