diff --git a/src/app/features/fee-editor/bitcoin/bitcoin-fee-editor.provider.tsx b/src/app/features/fee-editor/bitcoin/bitcoin-fee-editor.provider.tsx index e4ab9abcf7a..e5f6e462790 100644 --- a/src/app/features/fee-editor/bitcoin/bitcoin-fee-editor.provider.tsx +++ b/src/app/features/fee-editor/bitcoin/bitcoin-fee-editor.provider.tsx @@ -42,6 +42,7 @@ export function BitcoinFeeEditorProvider({ feeType="fee-rate" getCustomFee={getCustomFee} isLoadingFees={isLoading} + isSponsored={false} marketData={marketData} onGoBack={onGoBack} > diff --git a/src/app/features/fee-editor/components/selected-fee-item.tsx b/src/app/features/fee-editor/components/selected-fee-item.tsx index 9528dd31dd7..b17aa4c0653 100644 --- a/src/app/features/fee-editor/components/selected-fee-item.tsx +++ b/src/app/features/fee-editor/components/selected-fee-item.tsx @@ -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'; @@ -10,6 +12,7 @@ import { FeeValueItemLayout } from './fee-value-item.layout'; interface SelectedFeeItemProps { feeType: FeeType; isLoading: boolean; + isSponsored: boolean; marketData: MarketData; onEditFee(): void; selectedFee: Fee | null; @@ -17,15 +20,29 @@ interface SelectedFeeItemProps { export function SelectedFeeItem({ feeType, isLoading, + isSponsored, marketData, onEditFee, selectedFee, }: SelectedFeeItemProps) { + if (isSponsored) + return ( + + } />} my="space.02"> + + Sponsored fee + + + + + + + ); + if (isLoading || !selectedFee) return ( - Fee - + ); diff --git a/src/app/features/fee-editor/fee-editor.context.ts b/src/app/features/fee-editor/fee-editor.context.ts index 8c0b71717cb..cce28060032 100644 --- a/src/app/features/fee-editor/fee-editor.context.ts +++ b/src/app/features/fee-editor/fee-editor.context.ts @@ -21,6 +21,7 @@ interface FeeEditorContext { feeType: FeeType; loadedFee: Fee; isLoadingFees: boolean; + isSponsored: boolean; marketData: MarketData; fees: Fees; selectedFee: Fee; diff --git a/src/app/features/fee-editor/fee-editor.provider.tsx b/src/app/features/fee-editor/fee-editor.provider.tsx index e998150e5ef..4fb6baa8a98 100644 --- a/src/app/features/fee-editor/fee-editor.provider.tsx +++ b/src/app/features/fee-editor/fee-editor.provider.tsx @@ -20,6 +20,7 @@ interface FeeEditorProviderProps extends HasChildren { feeType: FeeType; getCustomFee(rate: number): Fee; isLoadingFees: boolean; + isSponsored: boolean; marketData: MarketData; onGoBack(): void; } @@ -30,6 +31,7 @@ export function FeeEditorProvider({ feeType, getCustomFee, isLoadingFees, + isSponsored, marketData, onGoBack, }: FeeEditorProviderProps) { @@ -45,6 +47,7 @@ export function FeeEditorProvider({ - {({ fees, isLoading, getCustomFee }) => { + {({ fees, isLoading, isSponsored, getCustomFee }) => { return ( diff --git a/src/app/features/fee-editor/stacks/stacks-fees-loader.tsx b/src/app/features/fee-editor/stacks/stacks-fees-loader.tsx index ef563d21291..b4624557d1f 100644 --- a/src/app/features/fee-editor/stacks/stacks-fees-loader.tsx +++ b/src/app/features/fee-editor/stacks/stacks-fees-loader.tsx @@ -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'; @@ -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 { @@ -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, + }); } diff --git a/src/app/features/nonce-editor/nonce-item.tsx b/src/app/features/nonce-editor/nonce-item.tsx index c2985635ffd..754baa10c59 100644 --- a/src/app/features/nonce-editor/nonce-item.tsx +++ b/src/app/features/nonce-editor/nonce-item.tsx @@ -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 ( diff --git a/src/app/features/rpc-transaction-request/stacks/use-sign-and-broadcast-stacks-transaction.ts b/src/app/features/rpc-transaction-request/stacks/use-sign-and-broadcast-stacks-transaction.ts index d05d5f1b0dd..2b0fa91e0fc 100644 --- a/src/app/features/rpc-transaction-request/stacks/use-sign-and-broadcast-stacks-transaction.ts +++ b/src/app/features/rpc-transaction-request/stacks/use-sign-and-broadcast-stacks-transaction.ts @@ -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, @@ -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)); diff --git a/src/app/features/rpc-transaction-request/transaction-actions/transaction-actions-with-spend.tsx b/src/app/features/rpc-transaction-request/transaction-actions/transaction-actions-with-spend.tsx index 01388df3fed..250a756f6b3 100644 --- a/src/app/features/rpc-transaction-request/transaction-actions/transaction-actions-with-spend.tsx +++ b/src/app/features/rpc-transaction-request/transaction-actions/transaction-actions-with-spend.tsx @@ -15,11 +15,13 @@ import { useRpcTransactionRequest } from '../use-rpc-transaction-request'; interface TransactionActionsWithSpendProps { isLoading: boolean; + isSponsored: boolean; txAmount: Money; onApprove(): Promise; } export function TransactionActionsWithSpend({ isLoading, + isSponsored, txAmount, onApprove, }: TransactionActionsWithSpendProps) { @@ -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 ( (() => { if (!origin) return TransactionErrorReason.ExpiredRequest; 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 8289bedf77d..3837d566835 100644 --- a/src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx +++ b/src/app/pages/rpc-send-transfer/rpc-send-transfer.tsx @@ -78,6 +78,7 @@ export function RpcSendTransfer() { 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() { @@ -58,6 +64,7 @@ export function RpcStxCallContract() { actions={ @@ -82,6 +90,7 @@ export function RpcStxDeployContract() { @@ -88,6 +96,7 @@ export function RpcStxTransferSip10Ft() { @@ -84,6 +92,7 @@ export function RpcStxTransferSip9Nft() { @@ -80,6 +88,7 @@ export function RpcStxTransferStx() { (); const [lastAddressChecked, setLastAddressChecked] = useState(); diff --git a/tests/specs/rpc-stx-call-contract/rpc-stx-call-contract.spec.ts b/tests/specs/rpc-sip30-requests/rpc-stx-call-contract.spec.ts similarity index 100% rename from tests/specs/rpc-stx-call-contract/rpc-stx-call-contract.spec.ts rename to tests/specs/rpc-sip30-requests/rpc-stx-call-contract.spec.ts diff --git a/tests/specs/rpc-stx-deploy-contract/rpc-stx-deploy-contract.spec.ts b/tests/specs/rpc-sip30-requests/rpc-stx-deploy-contract.spec.ts similarity index 100% rename from tests/specs/rpc-stx-deploy-contract/rpc-stx-deploy-contract.spec.ts rename to tests/specs/rpc-sip30-requests/rpc-stx-deploy-contract.spec.ts diff --git a/tests/specs/rpc-stx-message-signing/rpc-stx-message-signing.spec.ts b/tests/specs/rpc-sip30-requests/rpc-stx-message-signing.spec.ts similarity index 100% rename from tests/specs/rpc-stx-message-signing/rpc-stx-message-signing.spec.ts rename to tests/specs/rpc-sip30-requests/rpc-stx-message-signing.spec.ts diff --git a/tests/specs/rpc-stx-sign-transaction/rpc-stx-sign-transaction.spec.ts b/tests/specs/rpc-sip30-requests/rpc-stx-sign-transaction.spec.ts similarity index 100% rename from tests/specs/rpc-stx-sign-transaction/rpc-stx-sign-transaction.spec.ts rename to tests/specs/rpc-sip30-requests/rpc-stx-sign-transaction.spec.ts diff --git a/tests/specs/rpc-stx-transfer-sip10-ft/rpc-stx-transfer-sip10-ft.spec.ts b/tests/specs/rpc-sip30-requests/rpc-stx-transfer-sip10-ft.spec.ts similarity index 100% rename from tests/specs/rpc-stx-transfer-sip10-ft/rpc-stx-transfer-sip10-ft.spec.ts rename to tests/specs/rpc-sip30-requests/rpc-stx-transfer-sip10-ft.spec.ts diff --git a/tests/specs/rpc-stx-transfer-sip9-nft/rpc-stx-transfer-sip9-nft.spec.ts b/tests/specs/rpc-sip30-requests/rpc-stx-transfer-sip9-nft.spec.ts similarity index 100% rename from tests/specs/rpc-stx-transfer-sip9-nft/rpc-stx-transfer-sip9-nft.spec.ts rename to tests/specs/rpc-sip30-requests/rpc-stx-transfer-sip9-nft.spec.ts diff --git a/tests/specs/rpc-stx-transfer-stx/rpc-stx-transfer-stx.spec.tsx b/tests/specs/rpc-sip30-requests/rpc-stx-transfer-stx.spec.tsx similarity index 100% rename from tests/specs/rpc-stx-transfer-stx/rpc-stx-transfer-stx.spec.tsx rename to tests/specs/rpc-sip30-requests/rpc-stx-transfer-stx.spec.tsx