Skip to content

fix: sponsored txs #6289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function BitcoinFeeEditorProvider({
feeType="fee-rate"
getCustomFee={getCustomFee}
isLoadingFees={isLoading}
isSponsored={false}
marketData={marketData}
onGoBack={onGoBack}
>
Expand Down
23 changes: 20 additions & 3 deletions src/app/features/fee-editor/components/selected-fee-item.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Flex, HStack, styled } from 'leather-styles/jsx';

import type { MarketData } from '@leather.io/models';
import { Approver, Pressable } from '@leather.io/ui';
import { Approver, Avatar, Badge, Flag, PlaceholderIcon, Pressable } from '@leather.io/ui';

import { CryptoAssetItemPlaceholder } from '@app/components/crypto-asset-item/crypto-asset-item-placeholder';
import type { Fee, FeeType } from '@app/features/fee-editor/fee-editor.context';
Expand All @@ -10,22 +12,37 @@ import { FeeValueItemLayout } from './fee-value-item.layout';
interface SelectedFeeItemProps {
feeType: FeeType;
isLoading: boolean;
isSponsored: boolean;
marketData: MarketData;
onEditFee(): void;
selectedFee: Fee | null;
}
export function SelectedFeeItem({
feeType,
isLoading,
isSponsored,
marketData,
onEditFee,
selectedFee,
}: SelectedFeeItemProps) {
if (isSponsored)
return (
<Approver.Section>
<Flag img={<Avatar icon={<PlaceholderIcon />} />} my="space.02">
<HStack alignItems="center" justifyContent="space-between">
<styled.span textStyle="label.02">Sponsored fee</styled.span>
<Flex alignItems="center" gap="space.03">
<Badge label="FREE" variant="success" />
</Flex>
</HStack>
</Flag>
</Approver.Section>
);

if (isLoading || !selectedFee)
return (
<Approver.Section>
<Approver.Subheader>Fee</Approver.Subheader>
<CryptoAssetItemPlaceholder my="0" />
<CryptoAssetItemPlaceholder />
</Approver.Section>
);

Expand Down
1 change: 1 addition & 0 deletions src/app/features/fee-editor/fee-editor.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface FeeEditorContext {
feeType: FeeType;
loadedFee: Fee;
isLoadingFees: boolean;
isSponsored: boolean;
marketData: MarketData;
fees: Fees;
selectedFee: Fee;
Expand Down
3 changes: 3 additions & 0 deletions src/app/features/fee-editor/fee-editor.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface FeeEditorProviderProps extends HasChildren {
feeType: FeeType;
getCustomFee(rate: number): Fee;
isLoadingFees: boolean;
isSponsored: boolean;
marketData: MarketData;
onGoBack(): void;
}
Expand All @@ -30,6 +31,7 @@ export function FeeEditorProvider({
feeType,
getCustomFee,
isLoadingFees,
isSponsored,
marketData,
onGoBack,
}: FeeEditorProviderProps) {
Expand All @@ -45,6 +47,7 @@ export function FeeEditorProvider({
<FeeEditorContext.Provider
value={{
isLoadingFees,
isSponsored,
availableBalance,
marketData,
fees,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { StacksTransactionWire } from '@stacks/transactions';
import { type StacksTransactionWire } from '@stacks/transactions';

import type { MarketData, Money } from '@leather.io/models';

Expand All @@ -22,14 +22,15 @@ export function StacksFeeEditorProvider({
}: StacksFeeEditorProviderProps) {
return (
<StacksFeesLoader unsignedTx={unsignedTx}>
{({ fees, isLoading, getCustomFee }) => {
{({ fees, isLoading, isSponsored, getCustomFee }) => {
return (
<FeeEditorProvider
availableBalance={availableBalance}
fees={fees}
feeType="fee-value"
getCustomFee={getCustomFee}
isLoadingFees={isLoading}
isSponsored={isSponsored}
marketData={marketData}
onGoBack={onGoBack}
>
Expand Down
18 changes: 14 additions & 4 deletions src/app/features/fee-editor/stacks/stacks-fees-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { StacksTransactionWire } from '@stacks/transactions';
import { AuthType, type StacksTransactionWire } from '@stacks/transactions';

import { createMoneyFromDecimal } from '@leather.io/utils';

import { useCheckSbtcSponsorshipEligible } from '@app/query/sbtc/sponsored-transactions.hooks';
import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks';

import { type Fee, type Fees } from '../fee-editor.context';
Expand All @@ -10,16 +11,19 @@ import { useStacksFees } from './use-stacks-fees';
interface StacksFees {
fees: Fees;
isLoading: boolean;
isSponsored: boolean;
getCustomFee(value: number): Fee;
}

interface StacksFeesLoaderProps {
children({ fees, isLoading, getCustomFee }: StacksFees): React.JSX.Element;
children({ fees, isLoading, isSponsored, getCustomFee }: StacksFees): React.JSX.Element;
unsignedTx: StacksTransactionWire;
}
export function StacksFeesLoader({ children, unsignedTx }: StacksFeesLoaderProps) {
const { data: stxFees, isLoading } = useCalculateStacksTxFees(unsignedTx);
const { data: stxFees, isLoading: isLoadingFees } = useCalculateStacksTxFees(unsignedTx);
const fees = useStacksFees({ fees: stxFees });
const { isVerifying: isVerifyingSbtcSponsorship, result: sbtcSponsorshipEligibility } =
useCheckSbtcSponsorshipEligible({ baseTx: { transaction: unsignedTx }, stxFees });

function getCustomFee(feeValue: number): Fee {
return {
Expand All @@ -31,5 +35,11 @@ export function StacksFeesLoader({ children, unsignedTx }: StacksFeesLoaderProps
}

if (!fees) return null;
return children({ fees, isLoading, getCustomFee });
return children({
fees,
isLoading: isLoadingFees || isVerifyingSbtcSponsorship,
isSponsored:
sbtcSponsorshipEligibility?.isEligible || unsignedTx.auth.authType === AuthType.Sponsored,
getCustomFee,
});
}
4 changes: 2 additions & 2 deletions src/app/features/nonce-editor/nonce-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import {

import type { Nonce } from './nonce-editor.context';

interface SelectedFeeItemProps {
interface NonceItemProps {
nonce: Nonce;
onEditNonce(): void;
}
export function NonceItem({ nonce, onEditNonce }: SelectedFeeItemProps) {
export function NonceItem({ nonce, onEditNonce }: NonceItemProps) {
return (
<Approver.Section>
<Pressable onClick={onEditNonce} my="space.02">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';

import type { StacksTransactionWire, TxBroadcastResultRejected } from '@stacks/transactions';
import {
AuthType,
type StacksTransactionWire,
type TxBroadcastResultRejected,
} from '@stacks/transactions';

import {
RpcErrorCode,
Expand Down Expand Up @@ -50,6 +54,23 @@ export function useSignAndBroadcastStacksTransaction(method: RpcMethodNames) {
throw new Error('Error signing stacks transaction');
}

// If the transaction is sponsored, we do not broadcast it
const isSponsored = signedTx.auth?.authType === AuthType.Sponsored;
if (isSponsored) {
chrome.tabs.sendMessage(
tabId,
createRpcSuccessResponse(method, {
id: requestId,
result: {
txid: '',
transaction: signedTx.serialize(),
},
})
);
await delay(500);
closeWindow();
}

function onError(error: Error | string, reason?: TxBroadcastResultRejected['reason']) {
const message = isString(error) ? error : error.message;
if (reason) toast.error(getErrorMessage(reason));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import { useRpcTransactionRequest } from '../use-rpc-transaction-request';

interface TransactionActionsWithSpendProps {
isLoading: boolean;
isSponsored: boolean;
txAmount: Money;
onApprove(): Promise<void>;
}
export function TransactionActionsWithSpend({
isLoading,
isSponsored,
txAmount,
onApprove,
}: TransactionActionsWithSpendProps) {
Expand All @@ -33,7 +35,8 @@ export function TransactionActionsWithSpend({
}, [marketData, selectedFee?.txFee, txAmount]);

// TODO LEA-2537: Refactor error state
const isInsufficientBalance = availableBalance.amount.isLessThan(totalSpend.amount);
const isInsufficientBalance =
!isSponsored && availableBalance.amount.isLessThan(totalSpend.amount);

return (
<Approver.Actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ import { useCheckSbtcSponsorshipEligible } from '@app/query/sbtc/sponsored-trans
import { useStxCryptoAssetBalance } from '@app/query/stacks/balance/account-balance.hooks';
import { useCalculateStacksTxFees } from '@app/query/stacks/fees/fees.hooks';
import { useGetContractInterfaceQuery } from '@app/query/stacks/legacy-request-contract.query';
import { useNextNonce } from '@app/query/stacks/nonce/account-nonces.hooks';
import {
useCurrentStacksAccount,
useCurrentStacksAccountAddress,
} from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks';
import { useTransactionRequestState } from '@app/store/transactions/requests.hooks';
import { useUnsignedStacksTransactionBaseState } from '@app/store/transactions/transaction.hooks';

Expand All @@ -39,11 +35,9 @@ export function useTransactionError() {
const availableUnlockedBalance = filteredBalanceQuery.data?.unlockedBalance;

const unsignedTx = useUnsignedStacksTransactionBaseState();
const stxAddress = useCurrentStacksAccountAddress();
const { data: stxFees } = useCalculateStacksTxFees(unsignedTx.transaction);
const { data: nextNonce } = useNextNonce(stxAddress);
const { isVerifying: isVerifyingSbtcEligibilty, result: sbtcSponsorshipEligibility } =
useCheckSbtcSponsorshipEligible(unsignedTx, nextNonce, stxFees);
useCheckSbtcSponsorshipEligible({ baseTx: unsignedTx, stxFees });

return useMemo<TransactionErrorReason | void>(() => {
if (!origin) return TransactionErrorReason.ExpiredRequest;
Expand Down
1 change: 1 addition & 0 deletions src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function RpcSendTransfer() {
<FeeEditor.Trigger
feeType="fee-rate"
isLoading={isLoadingFees}
isSponsored={false}
marketData={marketData}
onEditFee={onUserActivatesFeeEditor}
selectedFee={selectedFee}
Expand Down
16 changes: 12 additions & 4 deletions src/app/pages/rpc-stx-call-contract/rpc-stx-call-contract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ import {

export function RpcStxCallContract() {
const { isLoadingBalance, network, publicKey } = useStacksRpcTransactionRequestContext();
const { availableBalance, isLoadingFees, marketData, onUserActivatesFeeEditor, selectedFee } =
useFeeEditorContext();
const {
availableBalance,
isLoadingFees,
isSponsored,
marketData,
onUserActivatesFeeEditor,
selectedFee,
} = useFeeEditorContext();
const { nonce, onUserActivatesNonceEditor } = useNonceEditorContext();
const signAndBroadcastTransaction = useSignAndBroadcastStacksTransaction(stxCallContract.method);
const convertToFiatAmount = useConvertCryptoCurrencyToFiatAmount('STX');
Expand All @@ -38,12 +44,12 @@ export function RpcStxCallContract() {
const txOptionsForBroadcast = useMemo(
() =>
getUnsignedStacksContractCallOptions({
fee: selectedFee.txFee,
fee: isSponsored ? createMoney(0, 'STX') : selectedFee.txFee,
network,
nonce,
publicKey,
}),
[network, nonce, publicKey, selectedFee.txFee]
[isSponsored, network, nonce, publicKey, selectedFee.txFee]
);

async function onApproveTransaction() {
Expand All @@ -58,6 +64,7 @@ export function RpcStxCallContract() {
actions={
<TransactionActionsWithSpend
isLoading={isLoadingBalance || isLoadingFees}
isSponsored={isSponsored}
// TODO: Calculate total request
txAmount={createMoney(0, 'STX')}
onApprove={onApproveTransaction}
Expand Down Expand Up @@ -85,6 +92,7 @@ export function RpcStxCallContract() {
<FeeEditor.Trigger
feeType="fee-value"
isLoading={isLoadingFees}
isSponsored={isSponsored}
marketData={marketData}
onEditFee={onUserActivatesFeeEditor}
selectedFee={selectedFee}
Expand Down
13 changes: 11 additions & 2 deletions src/app/pages/rpc-stx-deploy-contract/rpc-stx-deploy-contract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ import { getUnsignedStacksDeployContractOptions } from './rpc-stx-deploy-contrac

export function RpcStxDeployContract() {
const { address, isLoadingBalance, network, publicKey } = useStacksRpcTransactionRequestContext();
const { availableBalance, isLoadingFees, marketData, onUserActivatesFeeEditor, selectedFee } =
useFeeEditorContext();
const {
availableBalance,
isLoadingFees,
isSponsored,
marketData,
onUserActivatesFeeEditor,
selectedFee,
} = useFeeEditorContext();
const { nonce, onUserActivatesNonceEditor } = useNonceEditorContext();
const convertToFiatAmount = useConvertCryptoCurrencyToFiatAmount('STX');
const signAndBroadcastTransaction = useSignAndBroadcastStacksTransaction(
Expand All @@ -46,6 +52,7 @@ export function RpcStxDeployContract() {

async function onApproveTransaction() {
const unsignedTx = await generateStacksUnsignedTransaction(txOptionsForBroadcast);
if (isSponsored) unsignedTx.setFee(0);
await signAndBroadcastTransaction(unsignedTx);
}

Expand All @@ -57,6 +64,7 @@ export function RpcStxDeployContract() {
actions={
<TransactionActionsWithSpend
isLoading={isLoadingBalance || isLoadingFees}
isSponsored={isSponsored}
txAmount={createMoneyFromDecimal(0, 'STX')}
onApprove={onApproveTransaction}
/>
Expand All @@ -82,6 +90,7 @@ export function RpcStxDeployContract() {
<FeeEditor.Trigger
feeType="fee-value"
isLoading={isLoadingFees}
isSponsored={isSponsored}
marketData={marketData}
onEditFee={onUserActivatesFeeEditor}
selectedFee={selectedFee}
Expand Down
Loading
Loading