refactor(tx-signing): remove implicit signing of transactions from send-form

This commit is contained in:
kyranjamie
2021-11-24 14:10:41 +01:00
committed by kyranjamie
parent 088c716f22
commit 4f23ff0d01
19 changed files with 275 additions and 267 deletions

View File

@@ -82,7 +82,6 @@ export function useSignIn() {
setIsIdle,
]
);
const handleSetSeed = useCallback(
async (value: string, trim?: boolean) => {
const trimmed = trim ? value.trim() : value;

View File

@@ -38,23 +38,17 @@ function getErrorMessage(
const timeForApiToUpdate = 250;
interface UseSubmitTransactionCallbackArgs {
replaceByFee?: boolean;
onClose: () => void;
interface UseSubmitTransactionArgs {
loadingKey: string;
}
export function useSubmitTransactionCallback({
replaceByFee,
onClose,
loadingKey,
}: UseSubmitTransactionCallbackArgs) {
interface UseSubmitTransactionCallbackArgs {
replaceByFee?: boolean;
onClose(): void;
}
export function useSubmitTransactionCallback({ loadingKey }: UseSubmitTransactionArgs) {
const refreshAccountData = useRefreshAllAccountData();
const changeScreen = useChangeScreen();
<<<<<<< HEAD
const { setLatestNonce } = useWallet();
=======
const { doSetLatestNonce } = useWallet();
>>>>>>> 3be3b2d29 (refactor(tx-signing): use unsigned serialised txs for fee estimation)
const { setIsLoading, setIsIdle } = useLoading(loadingKey);
const stacksNetwork = useCurrentStacksNetworkState();
const { setActiveTabActivity } = useHomeTabs();
@@ -62,47 +56,46 @@ export function useSubmitTransactionCallback({
const externalTxid = useCurrentAccountTxIds();
const analytics = useAnalytics();
return useCallback<(tx: StacksTransaction) => Promise<void>>(
async transaction => {
setIsLoading();
const nonce = !replaceByFee && transaction.auth.spendingCondition?.nonce.toNumber();
try {
const response = await broadcastTransaction(transaction, stacksNetwork);
if (typeof response !== 'string') {
toast.error(getErrorMessage(response.reason));
onClose();
setIsIdle();
} else {
const txid = `0x${response}`;
if (!externalTxid.includes(txid)) {
await setLocalTxs({
rawTx: transaction.serialize().toString('hex'),
timestamp: todaysIsoDate(),
txid,
});
return useCallback(
({ replaceByFee, onClose }: UseSubmitTransactionCallbackArgs) =>
async (transaction: StacksTransaction) => {
setIsLoading();
const nonce = !replaceByFee && transaction.auth.spendingCondition?.nonce.toNumber();
try {
const response = await broadcastTransaction(transaction, stacksNetwork);
if (typeof response !== 'string') {
toast.error(getErrorMessage(response.reason));
onClose();
setIsIdle();
} else {
const txid = `0x${response}`;
if (!externalTxid.includes(txid)) {
await setLocalTxs({
rawTx: transaction.serialize().toString('hex'),
timestamp: todaysIsoDate(),
txid,
});
}
if (nonce) await setLatestNonce(nonce);
toast.success('Transaction submitted!');
void analytics.track('broadcast_transaction');
onClose();
setIsIdle();
changeScreen(RouteUrls.Home);
// switch active tab to activity
setActiveTabActivity();
await refreshAccountData(timeForApiToUpdate);
}
if (nonce) await setLatestNonce(nonce);
toast.success('Transaction submitted!');
void analytics.track('broadcast_transaction');
} catch (e) {
logger.error(e);
toast.error('Something went wrong');
onClose();
setIsIdle();
changeScreen(RouteUrls.Home);
// switch active tab to activity
setActiveTabActivity();
await refreshAccountData(timeForApiToUpdate);
}
} catch (e) {
logger.error(e);
toast.error('Something went wrong');
onClose();
setIsIdle();
}
},
},
[
setIsLoading,
replaceByFee,
stacksNetwork,
onClose,
setIsIdle,
externalTxid,
setLatestNonce,
@@ -116,18 +109,19 @@ export function useSubmitTransactionCallback({
}
interface UseHandleSubmitTransactionArgs {
transaction: StacksTransaction | null;
onClose: () => void;
loadingKey: string;
}
interface UseHandleSubmitTransactionReturnFn {
transaction: StacksTransaction;
replaceByFee?: boolean;
onClose(): void;
}
export function useHandleSubmitTransaction({
transaction,
onClose,
loadingKey,
replaceByFee = false,
}: UseHandleSubmitTransactionArgs) {
const callback = useSubmitTransactionCallback({ onClose, loadingKey, replaceByFee });
if (transaction) return () => callback(transaction);
return () => null;
export function useHandleSubmitTransaction({ loadingKey }: UseHandleSubmitTransactionArgs) {
const broadcastTxCallback = useSubmitTransactionCallback({ loadingKey });
return useCallback(
({ transaction, onClose, replaceByFee = false }: UseHandleSubmitTransactionReturnFn) =>
broadcastTxCallback({ onClose, replaceByFee })(transaction),
[broadcastTxCallback]
);
}

View File

@@ -98,7 +98,7 @@ function generateSignedStxTransferTx(args: GenerateSignedStxTransferTxArgs) {
return makeSTXTokenTransfer(options);
}
export type GenerateSignedTransactionOptions = GenerateSignedTxArgs<
type GenerateSignedTransactionOptions = GenerateSignedTxArgs<
ContractCallPayload | STXTransferPayload | ContractDeployPayload
>;
export async function generateSignedTransaction(options: GenerateSignedTransactionOptions) {

View File

@@ -119,10 +119,9 @@ function generateUnsignedStxTransferTx(args: GenerateUnsignedStxTransferTxArgs)
return makeUnsignedSTXTokenTransfer(options);
}
type GenerateUnsignedTransactionOptions = GenerateUnsignedTxArgs<
export type GenerateUnsignedTransactionOptions = GenerateUnsignedTxArgs<
ContractCallPayload | STXTransferPayload | ContractDeployPayload
>;
export async function generateUnsignedTransaction(options: GenerateUnsignedTransactionOptions) {
const { txData, publicKey, nonce, fee = 0 } = options;
const isValid = isTransactionTypeSupported(txData.txType);

View File

@@ -5,12 +5,15 @@ import { useCurrentNetwork } from '@common/hooks/use-current-network';
import { useDrawers } from '@common/hooks/use-drawers';
import { SpaceBetween } from '@components/space-between';
import { Caption } from '@components/typography';
import { useTxByteSizeState, useTxForSettingsState } from '@store/transactions/transaction.hooks';
import {
useTxByteSizeState,
useUnsignedTxForSettingsState,
} from '@store/transactions/transaction.hooks';
export function ShowEditNonceAction(): JSX.Element {
const { isTestnet, name } = useCurrentNetwork();
const { showEditNonce, setShowEditNonce } = useDrawers();
const [tx] = useTxForSettingsState();
const [tx] = useUnsignedTxForSettingsState();
const [, setTxBytes] = useTxByteSizeState();
return (

View File

@@ -4,14 +4,14 @@ import { Button, Stack } from '@stacks/ui';
import { LoadingKeys, useLoading } from '@common/hooks/use-loading';
import { useDrawers } from '@common/hooks/use-drawers';
import { useTxForSettingsState } from '@store/transactions/transaction.hooks';
import { useUnsignedTxForSettingsState } from '@store/transactions/transaction.hooks';
import { EditNonceField } from './edit-nonce-field';
export function EditNonceFormInner(): JSX.Element {
const { setFieldValue, handleSubmit } = useFormikContext();
const { isLoading } = useLoading(LoadingKeys.EDIT_NONCE_DRAWER);
const [transaction] = useTxForSettingsState();
const [transaction] = useUnsignedTxForSettingsState();
const nonce = transaction?.auth.spendingCondition?.nonce.toNumber();
const { setShowEditNonce } = useDrawers();

View File

@@ -6,14 +6,14 @@ import { Button, Stack } from '@stacks/ui';
import { LoadingKeys, useLoading } from '@common/hooks/use-loading';
import { nonceSchema } from '@common/validation/nonce-schema';
import { useCustomNonce } from '@store/transactions/nonce.hooks';
import { useTxForSettingsState } from '@store/transactions/transaction.hooks';
import { useUnsignedTxForSettingsState } from '@store/transactions/transaction.hooks';
import { EditNonceFormInner } from './edit-nonce-form-inner';
import { EditNonceField } from './edit-nonce-field';
// Not sure what this is doing?
const SuspenseOnMount = ({ onMountCallback, isEnabled }: any) => {
const [tx] = useTxForSettingsState();
const [tx] = useUnsignedTxForSettingsState();
useEffect(() => {
if (tx && isEnabled) {

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { AuthType } from '@stacks/transactions';
import { useUnsignedTxForSettingsState } from '@store/transactions/transaction.hooks';
interface TransactionFeeProps {
fee: number | string;
}
export function TransactionFee(props: TransactionFeeProps): JSX.Element | null {
const { fee } = props;
/** @deprecated */
const [transaction] = useUnsignedTxForSettingsState();
const isSponsored = transaction?.auth?.authType === AuthType.Sponsored;
return <>{isSponsored ? '🎉 sponsored' : fee} STX</>;
}

View File

@@ -5,12 +5,12 @@ import { Button, Stack } from '@stacks/ui';
import { LoadingKeys, useLoading } from '@common/hooks/use-loading';
interface SendTokensConfirmActionsProps {
onSubmit: () => void;
onUserConfirmBroadcast: () => void;
transaction: StacksTransaction | undefined;
}
export function SendTokensConfirmActions(props: SendTokensConfirmActionsProps): JSX.Element {
const { onSubmit: handleSubmit, transaction } = props;
const { onUserConfirmBroadcast: handleSubmit, transaction } = props;
const { isLoading } = useLoading(LoadingKeys.CONFIRM_DRAWER);
return (

View File

@@ -1,42 +1,32 @@
import React, { useCallback } from 'react';
import { Flex, Stack } from '@stacks/ui';
import React from 'react';
import { Stack } from '@stacks/ui';
import { useDrawers } from '@common/hooks/use-drawers';
import { BaseDrawer, BaseDrawerProps } from '@components/drawer';
import { LoadingKeys } from '@common/hooks/use-loading';
import { SpaceBetween } from '@components/space-between';
import { Caption } from '@components/typography';
import { TransactionFee } from '@components/fee-row/components/transaction-fee';
import { useHandleSubmitTransaction } from '@common/hooks/use-submit-stx-transaction';
import {
useLocalTransactionInputsState,
useTxForSettingsState,
useUnsignedTxForSettingsState,
} from '@store/transactions/transaction.hooks';
import { useFeeEstimationsState } from '@store/transactions/fees.hooks';
import { SendTokensConfirmActions } from './send-tokens-confirm-actions';
import { SendTokensConfirmDetails } from './send-tokens-confirm-details';
import { isTxSponsored } from '@common/transactions/transaction-utils';
export function SendTokensConfirmDrawer(props: BaseDrawerProps) {
const { isShowing, onClose } = props;
interface SendTokensConfirmDrawerProps extends BaseDrawerProps {
onUserSelectBroadcastTransaction(): void;
}
export function SendTokensConfirmDrawer(props: SendTokensConfirmDrawerProps) {
const { isShowing, onClose, onUserSelectBroadcastTransaction } = props;
const [txData] = useLocalTransactionInputsState();
const [transaction] = useTxForSettingsState();
const [transaction] = useUnsignedTxForSettingsState();
const { showEditNonce } = useDrawers();
const [, setFeeEstimations] = useFeeEstimationsState();
const isSponsored = transaction ? isTxSponsored(transaction) : false;
const broadcastTransaction = useHandleSubmitTransaction({
transaction: transaction || null,
onClose,
loadingKey: LoadingKeys.CONFIRM_DRAWER,
});
const broadcastTransactionAction = useCallback(async () => {
await broadcastTransaction();
setFeeEstimations([]);
}, [broadcastTransaction, setFeeEstimations]);
if (!isShowing || !transaction || !txData) return null;
return (
@@ -53,16 +43,14 @@ export function SendTokensConfirmDrawer(props: BaseDrawerProps) {
nonce={transaction?.auth.spendingCondition?.nonce.toNumber()}
/>
<SpaceBetween>
<Caption>
<Flex>Fees</Flex>
</Caption>
<Caption>Fees</Caption>
<Caption>
<TransactionFee isSponsored={isSponsored} fee={txData.fee} />
</Caption>
</SpaceBetween>
<SendTokensConfirmActions
onSubmit={() => broadcastTransactionAction()}
transaction={transaction}
onUserConfirmBroadcast={() => onUserSelectBroadcastTransaction()}
/>
</Stack>
</BaseDrawer>

View File

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import {
useLocalTransactionInputsState,
useTxForSettingsState,
useUnsignedTxForSettingsState,
} from '@store/transactions/transaction.hooks';
import { LoadingKeys, useLoading } from '@common/hooks/use-loading';
@@ -12,7 +12,7 @@ interface ShowDelayProps {
isShowing: boolean;
}
export const ShowDelay = ({ setShowing, beginShow, isShowing }: ShowDelayProps) => {
const [tx] = useTxForSettingsState();
const [tx] = useUnsignedTxForSettingsState();
const [txData] = useLocalTransactionInputsState();
const { setIsIdle } = useLoading(LoadingKeys.SEND_TOKENS_FORM);
useEffect(() => {

View File

@@ -1,5 +1,6 @@
import React, { memo, Suspense, useState } from 'react';
import React, { memo, Suspense, useCallback, useState } from 'react';
import { Formik } from 'formik';
import toast from 'react-hot-toast';
import { useSelectedAsset } from '@common/hooks/use-selected-asset';
import { LoadingKeys, useLoading } from '@common/hooks/use-loading';
@@ -10,12 +11,19 @@ import { Header } from '@components/header';
import { useChangeScreen } from '@common/hooks/use-change-screen';
import { HighFeeDrawer } from '@features/high-fee-drawer/high-fee-drawer';
import { useSendFormValidation } from '@pages/send-tokens/hooks/use-send-form-validation';
import { useLocalTransactionInputsState } from '@store/transactions/transaction.hooks';
import {
useLocalTransactionInputsState,
useSendFormUnsignedTxState,
useSignTransactionSoftwareWallet,
} from '@store/transactions/transaction.hooks';
import { SendTokensConfirmDrawer } from './components/send-tokens-confirm-drawer/send-tokens-confirm-drawer';
import { SendFormInner } from './components/send-form-inner';
import { ShowDelay } from './components/show-delay';
import { useResetNonceCallback } from './hooks/use-reset-nonce-callback';
import { useHandleSubmitTransaction } from '@common/hooks/use-submit-stx-transaction';
import { useFeeEstimationsState } from '@store/transactions/fees.hooks';
import { logger } from '@common/logger';
function SendTokensFormBase() {
const changeScreen = useChangeScreen();
@@ -25,16 +33,52 @@ function SendTokensFormBase() {
const [assetError, setAssetError] = useState<string | undefined>(undefined);
const { selectedAsset } = useSelectedAsset();
const sendFormSchema = useSendFormValidation({ setAssetError });
const [, setTxData] = useLocalTransactionInputsState();
const [_txData, setTxData] = useLocalTransactionInputsState();
const [beginShow, setBeginShow] = useState(false);
const resetNonceCallback = useResetNonceCallback();
const [, setFeeEstimations] = useFeeEstimationsState();
const transaction = useSendFormUnsignedTxState();
const handleConfirmDrawerOnClose = (setSubmitting: (value: boolean) => void) => {
const signSoftwareWalletTx = useSignTransactionSoftwareWallet();
const handleConfirmDrawerOnClose = useCallback(() => {
setShowing(false);
setSubmitting(false);
setTxData(null);
setIsIdle();
resetNonceCallback();
}, [resetNonceCallback, setIsIdle, setTxData]);
const broadcastTransactionFn = useHandleSubmitTransaction({
loadingKey: LoadingKeys.CONFIRM_DRAWER,
});
const broadcastTransactionAction = useCallback(async () => {
if (!transaction) {
logger.error('Cannot broadcast transaction, no tx in state');
toast.error('Unable to broadcast transaction');
return;
}
const signedTx = signSoftwareWalletTx(transaction);
await broadcastTransactionFn({
transaction: signedTx,
onClose() {
handleConfirmDrawerOnClose();
},
});
setFeeEstimations([]);
}, [
broadcastTransactionFn,
handleConfirmDrawerOnClose,
setFeeEstimations,
signSoftwareWalletTx,
transaction,
]);
const initalValues = {
amount: '',
recipient: '',
fee: '',
memo: '',
};
return (
@@ -42,39 +86,25 @@ function SendTokensFormBase() {
header={<Header title="Send" onClose={() => changeScreen(RouteUrls.PopupHome)} />}
>
<Formik
initialValues={{
amount: '',
recipient: '',
fee: '',
memo: '',
}}
initialErrors={{}}
onSubmit={values => {
if (
selectedAsset &&
values.amount &&
values.recipient &&
values.recipient !== '' &&
values.fee
) {
if (!assetError) {
setTxData({
amount: values.amount,
fee: values.fee,
memo: values.memo,
recipient: values.recipient,
});
setIsLoading();
setBeginShow(true);
}
}
}}
initialValues={initalValues}
validateOnChange={false}
validateOnBlur={false}
validateOnMount={false}
validationSchema={sendFormSchema}
onSubmit={values => {
if (selectedAsset && !assetError) {
setTxData({
amount: values.amount,
fee: values.fee,
memo: values.memo,
recipient: values.recipient,
});
setIsLoading();
setBeginShow(true);
}
}}
>
{formik => (
{() => (
<>
{!showHighFeeConfirmation && beginShow && (
<Suspense fallback={<></>}>
@@ -84,12 +114,14 @@ function SendTokensFormBase() {
<Suspense fallback={<></>}>
<SendFormInner assetError={assetError} />
</Suspense>
<Suspense fallback={<></>}>
<SendTokensConfirmDrawer
isShowing={isShowing && !showEditNonce}
onClose={() => handleConfirmDrawerOnClose(formik.setSubmitting)}
/>
</Suspense>
<SendTokensConfirmDrawer
isShowing={isShowing && !showEditNonce}
onClose={() => handleConfirmDrawerOnClose()}
onUserSelectBroadcastTransaction={async () => {
await broadcastTransactionAction();
}}
/>
<HighFeeDrawer />
</>
)}

View File

@@ -16,11 +16,7 @@ export const useReplaceByFeeSubmitCallBack = () => {
const [, setTxId] = useRawTxIdState();
const submitTransaction = useSubmitTransactionCallback({
onClose: () => {
setTxId(null);
},
loadingKey: LoadingKeys.INCREASE_FEE_DRAWER,
replaceByFee: true,
});
return useAtomCallback<void, { fee: number; nonce: number }>(
@@ -28,9 +24,14 @@ export const useReplaceByFeeSubmitCallBack = () => {
async get => {
const signedTx = await get(rawSignedTxState, true);
if (!signedTx) return;
await submitTransaction(signedTx);
await submitTransaction({
onClose: () => {
setTxId(null);
},
replaceByFee: true,
})(signedTx);
},
[submitTransaction]
[setTxId, submitTransaction]
)
);
};

View File

@@ -17,7 +17,6 @@ import {
import { serializePayload } from '@stacks/transactions/dist/payload';
import { stxToMicroStx, validateStacksAddress } from '@common/stacks-utils';
import { generateSignedTransaction } from '@common/transactions/generate-signed-txs';
import { stacksTransactionToHex, whenChainId } from '@common/transactions/transaction-utils';
import { currentNetworkState, currentStacksNetworkState } from '@store/network/networks';
import { currentAccountNonceState } from '@store/accounts/nonce';
@@ -25,9 +24,8 @@ import { currentAccountState, currentAccountStxAddressState } from '@store/accou
import { requestTokenPayloadState } from '@store/transactions/requests';
import { postConditionsState } from '@store/transactions/post-conditions';
import { localStacksTransactionInputsState } from '@store/transactions/local-transactions';
import { localTransactionState } from '@store/transactions/local-transactions';
import { sendFormUnsignedTxState } from '@store/transactions/local-transactions';
import { generateUnsignedTransaction } from '@common/transactions/generate-unsigned-txs';
import { customNonceState } from './nonce.hooks';
export const pendingTransactionState = atom<
@@ -45,30 +43,6 @@ export const pendingTransactionState = atom<
export const transactionAttachmentState = atom(get => get(pendingTransactionState)?.attachment);
/** @deprecated */
const signedStacksTransactionBaseState = atom(get => {
const account = get(currentAccountState);
const txData = get(pendingTransactionState);
const stxAddress = get(currentAccountStxAddressState);
const nonce = get(currentAccountNonceState);
const customNonce = get(customNonceState);
if (!account || !txData || !stxAddress || typeof nonce === 'undefined') return;
const txNonce = typeof customNonce === 'number' ? customNonce : nonce;
if (
txData.txType === TransactionTypes.ContractCall &&
!validateStacksAddress(txData.contractAddress)
) {
return { transaction: undefined, options: {} };
}
const options = {
fee: txData.fee,
senderKey: account.stxPrivateKey,
nonce: txNonce,
txData,
};
return generateSignedTransaction(options).then(transaction => ({ transaction, options }));
});
const unsignedStacksTransactionBaseState = atom(get => {
const account = get(currentAccountState);
const txData = get(pendingTransactionState);
@@ -93,46 +67,29 @@ const unsignedStacksTransactionBaseState = atom(get => {
return generateUnsignedTransaction(options).then(transaction => ({ transaction, options }));
});
/** @deprecated */
const signedStacksTransactionState = atom(get => {
const { transaction, options } = get(signedStacksTransactionBaseState);
if (!transaction) return;
return generateSignedTransaction({ ...options });
});
const unsignedStacksTransactionState = atom(get => {
export const unsignedStacksTransactionState = atom(get => {
const { transaction, options } = get(unsignedStacksTransactionBaseState);
if (!transaction) return;
return generateUnsignedTransaction({ ...options });
});
/** @deprecated */
export const signedTransactionState = atom(get => {
const signedTransaction = get(signedStacksTransactionState);
if (!signedTransaction) return;
const serialized = signedTransaction.serialize();
const txRaw = stacksTransactionToHex(signedTransaction);
export function prepareTxDetailsForBroadcast(tx: StacksTransaction) {
const serialized = tx.serialize();
const txRaw = stacksTransactionToHex(tx);
return {
serialized,
isSponsored: signedTransaction?.auth?.authType === AuthType.Sponsored,
nonce: signedTransaction?.auth.spendingCondition?.nonce.toNumber(),
fee: signedTransaction?.auth.spendingCondition?.fee?.toNumber(),
isSponsored: tx.auth?.authType === AuthType.Sponsored,
nonce: tx.auth.spendingCondition?.nonce.toNumber(),
fee: tx.auth.spendingCondition?.fee?.toNumber(),
txRaw,
};
});
}
export const unsignedTransactionState = atom(get => {
const unsignedTransaction = get(unsignedStacksTransactionState);
if (!unsignedTransaction) return;
const serialized = unsignedTransaction.serialize();
const txRaw = stacksTransactionToHex(unsignedTransaction);
return {
serialized,
isSponsored: unsignedTransaction?.auth?.authType === AuthType.Sponsored,
nonce: unsignedTransaction?.auth.spendingCondition?.nonce.toNumber(),
fee: unsignedTransaction?.auth.spendingCondition?.fee?.toNumber(),
txRaw,
};
return prepareTxDetailsForBroadcast(unsignedTransaction);
});
export const serializedUnsignedTransactionPayloadState = atom<string>(get => {
@@ -168,35 +125,23 @@ export const transactionBroadcastErrorState = atom<string | null>(null);
// like it could easily be error-prone. Say this value doesn't get reset when it should.
// The effect could be calamitous. A user would be changing settings for a stale, cached
// transaction they've long forgotten about.
/** @deprecated */
export const txForSettingsState = atom(get =>
get(pendingTransactionState) ? get(signedStacksTransactionState) : get(localTransactionState)
get(pendingTransactionState) ? get(unsignedStacksTransactionState) : get(sendFormUnsignedTxState)
);
// Using txForSettingsState which should be refactored
// with new transaction signing flow.
export const serializedTransactionPayloadState = atom<string>(get => {
const transaction = get(txForSettingsState);
const transaction = get(sendFormUnsignedTxState);
if (!transaction) return '';
const serializedTxPayload = serializePayload(transaction.payload);
return serializedTxPayload.toString('hex');
});
// Using txForSettingsState which should be refactored
// with new transaction signing flow.
export const estimatedTransactionByteLengthState = atom<number | null>(get => {
const transaction = get(txForSettingsState);
const transaction = get(sendFormUnsignedTxState);
if (!transaction) return null;
const serializedTx = transaction.serialize();
return serializedTx.byteLength;
});
export const txByteSize = atom<number | null>(null);
// dev tooling
postConditionsState.debugLabel = 'postConditionsState';
pendingTransactionState.debugLabel = 'pendingTransactionState';
transactionAttachmentState.debugLabel = 'transactionAttachmentState';
signedStacksTransactionState.debugLabel = 'signedStacksTransactionState';
signedTransactionState.debugLabel = 'signedTransactionState';
transactionNetworkVersionState.debugLabel = 'transactionNetworkVersionState';
transactionBroadcastErrorState.debugLabel = 'transactionBroadcastErrorState';

View File

@@ -8,6 +8,8 @@ import {
createEmptyAddress,
noneCV,
PostConditionMode,
pubKeyfromPrivKey,
publicKeyToString,
serializeCV,
someCV,
standardPrincipalCVFromAddress,
@@ -16,10 +18,6 @@ import {
import { ftUnshiftDecimals, stxToMicroStx } from '@common/stacks-utils';
import { TransactionFormValues } from '@common/transactions/transaction-utils';
import {
generateSignedTransaction,
GenerateSignedTransactionOptions,
} from '@common/transactions/generate-signed-txs';
import { makeFungibleTokenTransferState } from '@store/transactions/fungible-token-transfer';
import { selectedAssetStore } from '@store/assets/asset-search';
import { makePostCondition } from '@store/transactions/transaction.hooks';
@@ -27,13 +25,17 @@ import { currentAccountState, currentAccountStxAddressState } from '@store/accou
import { currentStacksNetworkState } from '@store/network/networks';
import { currentAccountNonceState } from '@store/accounts/nonce';
import { customNonceState } from '@store/transactions/nonce.hooks';
import {
generateUnsignedTransaction,
GenerateUnsignedTransactionOptions,
} from '@common/transactions/generate-unsigned-txs';
// This is the form state so can likely be removed from global store when we
// refactor transaction signing. Leaving for now to avoid conflicts but deprecating.
/** @deprecated */
export const localStacksTransactionInputsState = atom<TransactionFormValues | null>(null);
const stxTokenTransferTransactionState = atom(get => {
const stxTokenTransferUnsignedTxState = atom(get => {
const txData = get(localStacksTransactionInputsState);
const address = get(currentAccountStxAddressState);
const customNonce = get(customNonceState);
@@ -48,10 +50,9 @@ const stxTokenTransferTransactionState = atom(get => {
);
if (!account || typeof nonce === 'undefined') return;
const senderKey = account.stxPrivateKey;
const txNonce = typeof customNonce === 'number' ? customNonce : nonce;
const options: GenerateSignedTransactionOptions = {
senderKey,
const options: GenerateUnsignedTransactionOptions = {
publicKey: publicKeyToString(pubKeyfromPrivKey(account.stxPrivateKey)),
nonce: txNonce,
txData: {
txType: TransactionTypes.STXTransfer,
@@ -67,16 +68,16 @@ const stxTokenTransferTransactionState = atom(get => {
} as STXTransferPayload,
};
return generateSignedTransaction(options).then(transaction => {
return generateUnsignedTransaction(options).then(transaction => {
if (!transaction) return;
return generateSignedTransaction({
return generateUnsignedTransaction({
...options,
fee: stxToMicroStx(txData?.fee || 0).toNumber(),
});
});
});
const ftTokenTransferTransactionState = atom(get => {
const ftTokenTransferUnsignedTxState = atom(get => {
const txData = get(localStacksTransactionInputsState);
const address = get(currentAccountStxAddressState);
const customNonce = get(customNonceState);
@@ -88,16 +89,8 @@ const ftTokenTransferTransactionState = atom(get => {
const selectedAsset = get(selectedAssetStore);
if (!assetTransferState || !selectedAsset || !account) return;
const {
balances,
network,
senderKey,
assetName,
contractAddress,
contractName,
nonce,
stxAddress,
} = assetTransferState;
const { balances, network, assetName, contractAddress, contractName, nonce, stxAddress } =
assetTransferState;
const functionName = 'transfer';
@@ -149,31 +142,28 @@ const ftTokenTransferTransactionState = atom(get => {
postConditions,
postConditionMode: PostConditionMode.Deny,
network,
// Dummy public key to satisfy types
// This isn't a good parttern to follow, but much of this
// code will have to change with Ledger code anyway
publicKey: '',
publicKey: publicKeyToString(pubKeyfromPrivKey(account.stxPrivateKey)),
},
senderKey,
publicKey: publicKeyToString(pubKeyfromPrivKey(account.stxPrivateKey)),
nonce: txNonce,
} as const;
return generateSignedTransaction(options).then(transaction => {
return generateUnsignedTransaction(options).then(transaction => {
if (!transaction) return;
return generateSignedTransaction({
return generateUnsignedTransaction({
...options,
fee: stxToMicroStx(txData?.fee || 0).toNumber(),
});
});
});
const localTransactionIsStxTransferState = atom(get => {
const isSendFormSendingStx = atom(get => {
const selectedAsset = get(selectedAssetStore);
return selectedAsset?.type === 'stx';
});
export const localTransactionState = atom(get => {
return get(localTransactionIsStxTransferState)
? get(stxTokenTransferTransactionState)
: get(ftTokenTransferTransactionState);
export const sendFormUnsignedTxState = atom(get => {
return get(isSendFormSendingStx)
? get(stxTokenTransferUnsignedTxState)
: get(ftTokenTransferUnsignedTxState);
});

View File

@@ -48,9 +48,3 @@ export const transactionRequestStxAddressState = atom(
);
export const transactionRequestNetwork = atom(get => get(requestTokenPayloadState)?.network);
requestTokenPayloadState.debugLabel = 'requestTokenPayloadState';
requestTokenOriginState.debugLabel = 'requestTokenOriginState';
transactionRequestValidationState.debugLabel = 'transactionRequestValidationState';
transactionRequestStxAddressState.debugLabel = 'transactionRequestStxAddressState';
transactionRequestNetwork.debugLabel = 'transactionRequestNetwork';

View File

@@ -4,9 +4,11 @@ import { useAtom } from 'jotai';
import { useAtomCallback, useAtomValue, waitForAll } from 'jotai/utils';
import {
createAssetInfo,
createStacksPrivateKey,
FungibleConditionCode,
makeStandardFungiblePostCondition,
PostCondition,
TransactionSigner,
} from '@stacks/transactions';
import { todaysIsoDate } from '@common/date-utils';
@@ -16,24 +18,30 @@ import { broadcastTransaction } from '@common/transactions/broadcast-transaction
import { logger } from '@common/logger';
import { currentAccountState } from '@store/accounts';
import { currentNetworkState } from '@store/network/networks';
import { localStacksTransactionInputsState } from '@store/transactions/local-transactions';
import {
localStacksTransactionInputsState,
sendFormUnsignedTxState,
} from '@store/transactions/local-transactions';
import { currentAccountLocallySubmittedTxsState } from '@store/accounts/account-activity';
import { postConditionsState } from './post-conditions';
import { requestTokenState } from './requests';
import { useCurrentAccount } from '@store/accounts/account.hooks';
import { StacksTransaction } from '@stacks/connect/node_modules/@stacks/transactions';
import {
estimatedTransactionByteLengthState,
estimatedUnsignedTransactionByteLengthState,
prepareTxDetailsForBroadcast,
pendingTransactionState,
serializedTransactionPayloadState,
serializedUnsignedTransactionPayloadState,
signedTransactionState,
transactionAttachmentState,
transactionBroadcastErrorState,
txByteSize,
txForSettingsState,
unsignedStacksTransactionState,
unsignedTransactionState,
} from './index';
import { postConditionsState } from './post-conditions';
import { requestTokenState } from './requests';
export function usePendingTransaction() {
return useAtomValue(pendingTransactionState);
@@ -63,31 +71,47 @@ export function useEstimatedTransactionByteLengthState() {
return useAtomValue(estimatedTransactionByteLengthState);
}
export function useSignTransactionSoftwareWallet() {
const account = useCurrentAccount();
if (!account) throw new Error('Cannot sign a transaction without an account');
return useCallback(
(tx: StacksTransaction) => {
const signer = new TransactionSigner(tx);
signer.signOrigin(createStacksPrivateKey(account.stxPrivateKey));
return tx;
},
[account.stxPrivateKey]
);
}
// TODO: @kyranjamie
// only used for signed transactions, not send form
export function useTransactionBroadcast() {
const { setLatestNonce } = useWallet();
const signSoftwareWalletTx = useSignTransactionSoftwareWallet();
return useAtomCallback(
useCallback(
async (get, set) => {
const { account, signedTransaction, attachment, requestToken, network } = await get(
const { account, unsignedStacksTransaction, attachment, requestToken, network } = await get(
waitForAll({
// TODO: @kyranjamie
// Need to replace this mechanism to so that this broadcast hook
// doesn't implictly read the stxPrivateKey to create a signed tx
signedTransaction: signedTransactionState,
account: currentAccountState,
attachment: transactionAttachmentState,
requestToken: requestTokenState,
network: currentNetworkState,
unsignedStacksTransaction: unsignedStacksTransactionState,
}),
true
);
if (!account || !requestToken || !signedTransaction) {
if (!account || !requestToken || !unsignedStacksTransaction) {
set(transactionBroadcastErrorState, 'No pending transaction found.');
return;
}
try {
const { isSponsored, serialized, txRaw, nonce } = signedTransaction;
const signedTx = signSoftwareWalletTx(unsignedStacksTransaction);
const { isSponsored, serialized, txRaw, nonce } = prepareTxDetailsForBroadcast(signedTx);
const result = await broadcastTransaction({
isSponsored,
serialized,
@@ -110,7 +134,7 @@ export function useTransactionBroadcast() {
if (error instanceof Error) set(transactionBroadcastErrorState, error.message);
}
},
[setLatestNonce]
[setLatestNonce, signSoftwareWalletTx]
)
);
}
@@ -134,8 +158,26 @@ export function makePostCondition(options: PostConditionsOptions): PostCondition
);
}
export const useLocalTransactionInputsState = () => useAtom(localStacksTransactionInputsState);
export function useLocalTransactionInputsState() {
return useAtom(localStacksTransactionInputsState);
}
export const useTxForSettingsState = () => useAtom(txForSettingsState);
export function useSendFormUnsignedTxState() {
return useAtomValue(sendFormUnsignedTxState);
}
export const useTxByteSizeState = () => useAtom(txByteSize);
/**
* @deprecated
* Do not use implicit state-driven atom to get a "active" `StacksTransaction`
* This atom uses the presence of `pendingTransaction` to determine whether the
* "current" tx is either from a dApp, or the send form.
*
* Instead, be explicit when dealing with transaction broadcasts.
*/
export function useUnsignedTxForSettingsState() {
return useAtom(txForSettingsState);
}
export function useTxByteSizeState() {
return useAtom(txByteSize);
}

View File

@@ -3,7 +3,7 @@ const baseConfig = require('./webpack.config.base');
const config = {
...baseConfig,
devtool: 'eval-source-map',
devtool: 'eval-cheap-module-source-map',
mode: 'development',
output: {
...baseConfig.output,

View File

@@ -16025,7 +16025,12 @@ typeforce@^1.11.3, typeforce@^1.11.5:
resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc"
integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==
typescript@4.4.4, typescript@^4.1.2:
typescript@4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
typescript@^4.1.2:
version "4.4.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==