Skip to content

Commit

Permalink
feat: compliance checks
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Mar 27, 2024
1 parent 541c27a commit 9499696
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 19 deletions.
24 changes: 24 additions & 0 deletions src/app/common/validation/forms/compliance-validators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { isString } from 'formik';
import * as yup from 'yup';

import { checkEntityAddressIsCompliant } from '@app/query/common/compliance-checker/compliance-checker.query';

export function complianceValidator(
shouldCheckCompliance: yup.StringSchema<string, yup.AnyObject>
) {
return yup.string().test({
message: 'Compliance check failed',
async test(value) {
if (!isString(value)) return false;

if (!shouldCheckCompliance.isValidSync(value)) return true;

try {
const resp = await checkEntityAddressIsCompliant(value);
return !resp.isOnSanctionsList;
} catch (e) {
return true;
}
},
});
}
4 changes: 3 additions & 1 deletion src/app/common/validation/forms/recipient-validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {
stxAddressNetworkValidator,
stxAddressValidator,
} from './address-validators';
import { complianceValidator } from './compliance-validators';

export function stxRecipientValidator(
currentAddress: string,
currentNetwork: NetworkConfiguration
) {
return stxAddressValidator(FormErrorMessages.InvalidAddress)
.concat(stxAddressNetworkValidator(currentNetwork))
.concat(notCurrentAddressValidator(currentAddress || ''));
.concat(notCurrentAddressValidator(currentAddress || ''))
.concat(complianceValidator(stxAddressValidator(FormErrorMessages.InvalidAddress)));
}
19 changes: 11 additions & 8 deletions src/app/components/loaders/bitcoin-account-loader.tsx
Original file line number Diff line number Diff line change
@@ -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<P2Ret>; taproot: Signer<P2TROut> }): 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 });
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { LedgerIcon } from '@app/ui/icons/ledger-icon';
interface ConnectLedgerAssetBtnProps {
chain: SupportedBlockchains;
}

export function ConnectLedgerAssetBtn({ chain }: ConnectLedgerAssetBtnProps) {
const navigate = useNavigate();

Expand Down
8 changes: 4 additions & 4 deletions src/app/features/collectibles/collectibles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -47,22 +47,22 @@ export function Collectibles() {
isLoadingMore={isLoadingMore}
onRefresh={() => void queryClient.refetchQueries({ type: 'active' })}
>
<CurrentBitcoinAccountLoader>{() => <AddCollectible />}</CurrentBitcoinAccountLoader>
<CurrentBitcoinSignerLoader>{() => <AddCollectible />}</CurrentBitcoinSignerLoader>

{isNftMetadataEnabled && (
<CurrentStacksAccountLoader>
{account => <StacksCryptoAssets account={account} />}
</CurrentStacksAccountLoader>
)}

<CurrentBitcoinAccountLoader>
<CurrentBitcoinSignerLoader>
{() => (
<>
<Stamps />
<Ordinals setIsLoadingMore={setIsLoadingMore} />
</>
)}
</CurrentBitcoinAccountLoader>
</CurrentBitcoinSignerLoader>
</CollectiblesLayout>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 5 additions & 2 deletions src/app/features/psbt-signer/psbt-signer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { closeWindow, isError } from '@shared/utils';
import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { SignPsbtArgs } from '@app/common/psbt/requests';
import { PopupHeader } from '@app/features/current-account/popup-header';
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';
Expand All @@ -22,7 +23,7 @@ import { PsbtRequestHeader } from './components/psbt-request-header';
import { PsbtRequestRaw } from './components/psbt-request-raw';
import { PsbtRequestSighashWarningLabel } from './components/psbt-request-sighash-warning-label';
import { PsbtSignerLayout } from './components/psbt-signer.layout';
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';

Expand Down Expand Up @@ -71,12 +72,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,
Expand Down
3 changes: 3 additions & 0 deletions src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js';
import { createMoney } from '@shared/models/money.model';

import { formatMoneyPadded } from '@app/common/money/format-money';
import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

import { SendTransferActions } from './components/send-transfer-actions';
Expand All @@ -16,6 +17,8 @@ export function RpcSendTransfer() {
const amountAsMoney = createMoney(new BigNumber(amount), 'BTC');
const formattedMoney = formatMoneyPadded(amountAsMoney);

useBreakOnNonCompliantEntity(address);

return (
<>
<SendTransferHeader amount={formattedMoney} origin={origin} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -9,8 +10,11 @@ export function RpcSignStacksTransaction() {
stacksTransaction,
disableNonceSelection,
isMultisig,
txSender,
} = useRpcSignStacksTransaction();

useBreakOnNonCompliantEntity(txSender);

return (
<StacksTransactionSigner
onSignStacksTransaction={onSignStacksTransaction}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { closeWindow } from '@shared/utils';

import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { useRejectIfLedgerWallet } from '@app/common/rpc-helpers';
import { getTxSenderAddress } from '@app/common/transactions/stacks/transaction.utils';
import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks';

function useRpcSignStacksTransactionParams() {
Expand All @@ -30,13 +31,14 @@ function useRpcSignStacksTransactionParams() {
requestId,
isMultisig: isMultisig === 'true',
stacksTransaction: deserializeTransaction(txHex),
txSender: getTxSenderAddress(deserializeTransaction(txHex)) ?? '',
}),
[origin, txHex, requestId, isMultisig, tabId]
);
}

export function useRpcSignStacksTransaction() {
const { origin, requestId, tabId, stacksTransaction, isMultisig } =
const { origin, requestId, tabId, stacksTransaction, isMultisig, txSender } =
useRpcSignStacksTransactionParams();
const signStacksTx = useSignStacksTransaction();
const wasSignedByOtherOwners =
Expand All @@ -49,6 +51,7 @@ export function useRpcSignStacksTransaction() {
disableNonceSelection: wasSignedByOtherOwners,
stacksTransaction,
isMultisig,
txSender,
async onSignStacksTransaction(fee: number, nonce: number) {
stacksTransaction.setFee(fee);
stacksTransaction.setNonce(nonce);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
btcAddressNetworkValidator,
btcAddressValidator,
} from '@app/common/validation/forms/address-validators';
import { complianceValidator } from '@app/common/validation/forms/compliance-validators';
import { useNumberOfInscriptionsOnUtxo } from '@app/query/bitcoin/ordinals/inscriptions.hooks';
import { useSignBitcoinTx } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
Expand Down Expand Up @@ -139,6 +140,7 @@ export function useSendInscriptionForm() {
.string()
.required(FormErrorMessages.AddressRequired)
.concat(btcAddressValidator())
.concat(complianceValidator(btcAddressValidator()))
.concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork)),
}),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
btcInsufficientBalanceValidator,
btcMinimumSpendValidator,
} from '@app/common/validation/forms/amount-validators';
import { complianceValidator } from '@app/common/validation/forms/compliance-validators';
import {
btcAmountPrecisionValidator,
currencyAmountValidator,
Expand Down Expand Up @@ -75,6 +76,7 @@ export function useBtcSendForm() {
.concat(btcAddressValidator())
.concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.bitcoinNetwork))
.concat(notCurrentAddressValidator(nativeSegwitSigner.address || ''))
.concat(complianceValidator(btcAddressValidator()))
.required('Enter a bitcoin address'),
}),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { type UseQueryOptions, useQueries } from '@tanstack/react-query';
import axios from 'axios';

import type { BitcoinNetworkModes } from '@shared/constants';
import { ensureArray } from '@shared/utils';

import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';

const checkApi = 'https://api.chainalysis.com/api/risk/v2/entities';

const headers = {
// Known public key, do not open a vulnerability report for this
Token: '51d3c7529eb08a8c62d41d70d006bdcd4248150fbd6826d5828ac908e7c12073',
};

async function registerEntityAddressComplianceCheck(address: string) {
const resp = await axios.post(checkApi, { address }, { headers });
return resp.data;
}

async function checkEntityAddressComplianceCheck(address: string) {
const resp = await axios.get(checkApi + '/' + address, { headers });
return resp.data;
}

interface ComplianceReport {
risk: 'None' | 'Low' | 'Moderate' | 'Severe';
isOnSanctionsList: boolean;
}
export async function checkEntityAddressIsCompliant(address: string): Promise<ComplianceReport> {
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<ComplianceReport> {
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');
}
}
2 changes: 2 additions & 0 deletions src/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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;

Expand Down

0 comments on commit 9499696

Please sign in to comment.