Skip to content

Commit

Permalink
feat: sip30 stx call contract, closes LEA-1954
Browse files Browse the repository at this point in the history
  • Loading branch information
fbwoolf committed Feb 8, 2025
1 parent ae32c37 commit 1314ad6
Show file tree
Hide file tree
Showing 18 changed files with 435 additions and 37 deletions.
3 changes: 1 addition & 2 deletions src/app/common/hooks/use-loading.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useLoadingState } from '@app/store/ui/ui.hooks';

export enum LoadingKeys {
SUBMIT_SEND_FORM_TRANSACTION = 'loading/SUBMIT_SEND_FORM_TRANSACTION',
SUBMIT_STACKS_TRANSACTION = 'loading/SUBMIT_STACKS_TRANSACTION',
SUBMIT_SWAP_TRANSACTION = 'loading/SUBMIT_SWAP_TRANSACTION',
SUBMIT_TRANSACTION_REQUEST = 'loading/SUBMIT_TRANSACTION_REQUEST',
}

export function useLoading(key: string) {
Expand Down
9 changes: 5 additions & 4 deletions src/app/common/hooks/use-submit-stx-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ interface UseSubmitTransactionCallbackArgs {
export function useSubmitTransactionCallback({ loadingKey }: UseSubmitTransactionArgs) {
const toast = useToast();
const refreshAccountData = useRefreshAllAccountData();

const { setIsLoading, setIsIdle } = useLoading(loadingKey);
const stacksNetwork = useCurrentStacksNetworkState();

Expand All @@ -41,7 +40,7 @@ export function useSubmitTransactionCallback({ loadingKey }: UseSubmitTransactio
logger.error('Transaction broadcast', response);
if (response.reason) toast.error(getErrorMessage(response.reason));
onError(response.error);
setIsIdle();
return setIsIdle();
} else {
logger.info('Transaction broadcast', response);

Expand All @@ -50,14 +49,16 @@ export function useSubmitTransactionCallback({ loadingKey }: UseSubmitTransactio
void analytics.track('broadcast_transaction', {
symbol: 'stx',
});
onSuccess(safelyFormatHexTxid(response.txid));
const txid = safelyFormatHexTxid(response.txid);
onSuccess(txid);
setIsIdle();
await refreshAccountData(timeForApiToUpdate);
return { txid, transaction };
}
} catch (error) {
logger.error('Transaction callback', { error });
onError(isError(error) ? error : { name: '', message: '' });
setIsIdle();
return setIsIdle();
}
},
[setIsLoading, stacksNetwork, toast, setIsIdle, refreshAccountData]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ function generateUnsignedContractCallTx(args: GenerateUnsignedContractCallTxArgs
network,
sponsored,
} satisfies UnsignedContractCallOptions;

return makeUnsignedContractCall(options);
}

Expand Down
1 change: 1 addition & 0 deletions src/app/common/transactions/stacks/transaction.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,5 @@ export function isPendingTx(tx: StacksTx) {
export enum StacksTransactionActionType {
Cancel = 'cancel',
IncreaseFee = 'increase-fee',
RpcRequest = 'rpc-request',
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,14 @@ async function simulateShortDelayToAvoidUndefinedTabId() {
}

interface UseStacksBroadcastTransactionArgs {
token: CryptoCurrency;
decimals?: number;
actionType?: StacksTransactionActionType;
decimals?: number;
token: CryptoCurrency;
}

export function useStacksBroadcastTransaction({
token,
decimals,
actionType,
decimals,
token,
}: UseStacksBroadcastTransactionArgs) {
const signStacksTransaction = useSignStacksTransaction();
const [isBroadcasting, setIsBroadcasting] = useState(false);
Expand All @@ -46,11 +45,12 @@ export function useStacksBroadcastTransaction({
const navigate = useNavigate();
const toast = useToast();

const isIncreaseFeeTransaction = actionType === StacksTransactionActionType.IncreaseFee;
const isCancelTransaction = actionType === StacksTransactionActionType.Cancel;
const isIncreaseFeeTransaction = actionType === StacksTransactionActionType.IncreaseFee;
const isRpcRequest = actionType === StacksTransactionActionType.RpcRequest;

const broadcastTransactionFn = useSubmitTransactionCallback({
loadingKey: LoadingKeys.SUBMIT_SEND_FORM_TRANSACTION,
loadingKey: LoadingKeys.SUBMIT_STACKS_TRANSACTION,
});

return useMemo(() => {
Expand All @@ -66,7 +66,7 @@ export function useStacksBroadcastTransaction({
});
}
if (txId) {
if (isIncreaseFeeTransaction || isCancelTransaction) {
if (isCancelTransaction || isIncreaseFeeTransaction || isRpcRequest) {
navigate(RouteUrls.Activity);
return;
}
Expand All @@ -76,7 +76,7 @@ export function useStacksBroadcastTransaction({
':txId',
`${txId}`
),
formSentSummaryTxState(txId, signedTx, decimals)
formSentSummaryTxState ? formSentSummaryTxState(txId, signedTx, decimals) : {}
);
}
}
Expand All @@ -94,19 +94,17 @@ export function useStacksBroadcastTransaction({
await simulateShortDelayToAvoidUndefinedTabId();
handlePreviewSuccess(signedTx);
} else {
await broadcastTransactionFn({
return await broadcastTransactionFn({
onError(e: Error | string) {
const message = isString(e) ? e : e.message;
navigate(RouteUrls.TransactionBroadcastError, { state: { message } });
},
onSuccess(txId) {
handlePreviewSuccess(signedTx, txId);
if (isIncreaseFeeTransaction) {
toast.success('Fee increased successfully');
}
if (isCancelTransaction) {
toast.success('Transaction cancelled successfully');
}
if (isCancelTransaction) return toast.success('Transaction cancelled successfully');
if (isIncreaseFeeTransaction) return toast.success('Fee increased successfully');
if (isRpcRequest) return toast.success('Transaction submitted!');
return;
},
replaceByFee: false,
})(signedTx);
Expand All @@ -126,7 +124,7 @@ export function useStacksBroadcastTransaction({
const signedTx = await signStacksTransaction(unsignedTx);
// TODO: Maybe better error handling here?
if (!signedTx) return;
await broadcastTransactionAction(signedTx);
return await broadcastTransactionAction(signedTx);
} catch (e) {}
}

Expand All @@ -138,14 +136,15 @@ export function useStacksBroadcastTransaction({
isBroadcasting,
requestToken,
tabId,
isCancelTransaction,
isIncreaseFeeTransaction,
isRpcRequest,
navigate,
token,
formSentSummaryTxState,
decimals,
toast,
broadcastTransactionFn,
signStacksTransaction,
isIncreaseFeeTransaction,
isCancelTransaction,
]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
formatMoney,
i18nFormatCurrency,
isDefined,
isEmptyString,
microStxToStx,
} from '@leather.io/utils';

Expand All @@ -38,6 +39,9 @@ export function useStacksTransactionSummary(token: CryptoCurrency) {
// TODO: unsafe type assumption
const tokenMarketData = useCryptoCurrencyMarketDataMeanAverage(token as 'BTC' | 'STX');

// RPC requests do not show a review step
if (isEmptyString(token)) return { formSentSummaryTxState: null, formReviewTxSummary: null };

function formSentSummaryTxState(
txId: string,
signedTx: StacksTransactionWire,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import * as yup from 'yup';
import { HIGH_FEE_WARNING_LEARN_MORE_URL_STX } from '@leather.io/constants';
import { FeeTypes } from '@leather.io/models';
import {
defaultStacksFees,
useCalculateStacksTxFees,
useNextNonce,
useStxCryptoAssetBalance,
} from '@leather.io/query';
import { Link } from '@leather.io/ui';
import { stxToMicroStx } from '@leather.io/utils';
import { isDefined, stxToMicroStx } from '@leather.io/utils';

import { StacksTransactionFormValues } from '@shared/models/form.model';
import { RouteUrls } from '@shared/route-urls';
Expand All @@ -40,7 +41,7 @@ import { MinimalErrorMessage } from './minimal-error-message';
import { StacksTxSubmitAction } from './submit-action';

interface StacksTransactionSignerProps {
stacksTransaction: StacksTransactionWire;
stacksTransaction?: StacksTransactionWire;
disableFeeSelection?: boolean;
disableNonceSelection?: boolean;
isMultisig: boolean;
Expand Down Expand Up @@ -91,7 +92,8 @@ export function StacksTransactionSigner({
nonce: nonceValidator,
});

const isNonceAlreadySet = !Number.isNaN(transactionRequest.nonce);
const isNonceAlreadySet =
isDefined(transactionRequest.nonce) && !Number.isNaN(transactionRequest.nonce);

const initialValues: StacksTransactionFormValues = {
fee: '',
Expand Down Expand Up @@ -123,7 +125,7 @@ export function StacksTransactionSigner({

{!isNonceAlreadySet && <NonceSetter />}
<FeeForm
fees={stxFees}
fees={stxFees ?? defaultStacksFees}
sbtcSponsorshipEligibility={{ isEligible: false }}
defaultFeeValue={Number(transactionRequest?.fee || 0)}
disableFeeSelection={disableFeeSelection}
Expand Down
23 changes: 23 additions & 0 deletions src/app/pages/rpc-stx-call-contract/rpc-stx-call-contract.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { StacksHighFeeWarningContainer } from '@app/features/stacks-high-fee-warning/stacks-high-fee-warning-container';
import { StacksTransactionSigner } from '@app/features/stacks-transaction-request/stacks-transaction-signer';
import { useBreakOnNonCompliantEntity } from '@app/query/common/compliance-checker/compliance-checker.query';

import { useRpcStxCallContract } from './use-rpc-stx-call-contract';

export function RpcStxCallContract() {
const { onSignStacksTransaction, onCancel, stacksTransaction, txSender } =
useRpcStxCallContract();

useBreakOnNonCompliantEntity(txSender);

return (
<StacksHighFeeWarningContainer>
<StacksTransactionSigner
onSignStacksTransaction={onSignStacksTransaction}
onCancel={onCancel}
isMultisig={false}
stacksTransaction={stacksTransaction}
/>
</StacksHighFeeWarningContainer>
);
}
119 changes: 119 additions & 0 deletions src/app/pages/rpc-stx-call-contract/use-rpc-stx-call-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { useMemo } from 'react';
import { useAsync } from 'react-async-hook';

import { useNextNonce } from '@leather.io/query';
import { RpcErrorCode } from '@leather.io/rpc';
import { isUndefined } from '@leather.io/utils';

import { logger } from '@shared/logger';
import { makeRpcErrorResponse, makeRpcSuccessResponse } from '@shared/rpc/rpc-methods';
import { closeWindow } from '@shared/utils';
import {
type TransactionPayload,
getLegacyTransactionPayloadFromToken,
} from '@shared/utils/legacy-requests';

import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { initialSearchParams } from '@app/common/initial-search-params';
import {
type GenerateUnsignedTransactionOptions,
generateUnsignedTransaction,
} from '@app/common/transactions/stacks/generate-unsigned-txs';
import { getTxSenderAddress } from '@app/common/transactions/stacks/transaction.utils';
import { useStacksBroadcastTransaction } from '@app/features/stacks-transaction-request/hooks/use-stacks-broadcast-transaction';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useCurrentStacksNetworkState } from '@app/store/networks/networks.hooks';

function useRpcStxCallContractParams() {
const { origin, tabId } = useDefaultRequestParams();
const requestId = initialSearchParams.get('requestId');
const request = initialSearchParams.get('request');

if (!origin || !request || !requestId) throw new Error('Invalid params');

return useMemo(
() => ({
origin,
tabId: tabId ?? 0,
request: getLegacyTransactionPayloadFromToken(request),
requestId,
}),
[origin, tabId, request, requestId]
);
}

function useUnsignedStacksTransactionFromRequest(request: TransactionPayload) {
const account = useCurrentStacksAccount();
const { data: nextNonce } = useNextNonce(account?.address ?? '');
const network = useCurrentStacksNetworkState();

const tx = useAsync(async () => {
if (isUndefined(account) || isUndefined(nextNonce)) return;

const options: GenerateUnsignedTransactionOptions = {
publicKey: account.stxPublicKey,
txData: {
...request,
network: request.network ?? network,
sponsored: request.sponsored ?? false,
},
fee: request.fee ?? 0,
nonce: request.nonce ?? nextNonce.nonce,
};
return generateUnsignedTransaction(options);
}, [account, network, nextNonce, request]);

return tx.result;
}

export function useRpcStxCallContract() {
const { origin, request, requestId, tabId } = useRpcStxCallContractParams();
const stacksTransaction = useUnsignedStacksTransactionFromRequest(request);
const { stacksBroadcastTransaction } = useStacksBroadcastTransaction({ token: '' });

return useMemo(
() => ({
origin,
txSender: stacksTransaction ? getTxSenderAddress(stacksTransaction) : '',
stacksTransaction,
async onSignStacksTransaction(fee: number, nonce: number) {
if (!stacksTransaction) {
return logger.error('No stacks transaction to sign');
}

stacksTransaction.setFee(fee);
stacksTransaction.setNonce(nonce);

const result = await stacksBroadcastTransaction(stacksTransaction);
if (!result) {
throw new Error('Error broadcasting stacks transaction');
}

chrome.tabs.sendMessage(
tabId,
makeRpcSuccessResponse('stx_callContract', {
id: requestId,
result: {
txid: result.txid,
transaction: result.transaction.serialize(),
},
})
);
closeWindow();
},
onCancel() {
chrome.tabs.sendMessage(
tabId,
makeRpcErrorResponse('stx_callContract', {
id: requestId,
error: {
message: 'User denied signing stacks transaction',
code: RpcErrorCode.USER_REJECTION,
},
})
);
},
}),
[origin, requestId, stacksBroadcastTransaction, stacksTransaction, tabId]
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export function StacksSendFormConfirmation() {
const { formReviewTxSummary } = useStacksTransactionSummary(
symbol.toUpperCase() as CryptoCurrency
);

if (!formReviewTxSummary) return null;

const {
txValue,
txFiatValue,
Expand Down
Loading

0 comments on commit 1314ad6

Please sign in to comment.