diff --git a/config/wallet-config.json b/config/wallet-config.json index 8fa67c0f4e..af25046cd4 100644 --- a/config/wallet-config.json +++ b/config/wallet-config.json @@ -96,8 +96,12 @@ "sbtc": { "enabled": true, "swapsEnabled": true, + "sponsorshipsEnabled": true, + "sponsorshipApiUrl": { + "mainnet": "https://sponsor.leather.io", + "testnet": "http://testnet-13-60-14-218.nip.io" + }, "emilyApiUrl": "https://beta.sbtc-emily.com", - "leatherSponsorApiUrl": "http://testnet-13-60-14-218.nip.io", "showPromoLinkOnNetworks": ["testnet", "sbtcTestnet"], "contracts": { "mainnet": { diff --git a/config/wallet-config.schema.json b/config/wallet-config.schema.json index 0e477c950c..ed606fa947 100644 --- a/config/wallet-config.schema.json +++ b/config/wallet-config.schema.json @@ -158,6 +158,10 @@ "type": "boolean", "description": "Determines whether or not SBTC is enabled" }, + "sponsorshipsEnabled": { + "type": "boolean", + "description": "Determines whether or not sponsored sBTC transactions are enabled" + }, "swapsEnabled": { "type": "boolean", "description": "Determines whether or not the stacks swaps feature is enabled for sBTC" @@ -166,9 +170,18 @@ "type": "string", "description": "URL for the Emily API" }, - "leatherSponsorApiUrl": { - "type": "string", - "description": "URL for the Leather Sponsor API" + "sponsorshipApiUrl": { + "type": "object", + "properties": { + "mainnet": { + "type": "string", + "description": "Mainnet URL for the Leather Sponsor API" + }, + "testnet": { + "type": "string", + "description": "Testnet URL for the Leather Sponsor API" + } + } }, "showPromoLinkOnNetworks": { "type": "array", diff --git a/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts b/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts index fce97c9e6a..0b4f4b96a5 100644 --- a/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts +++ b/src/app/features/stacks-transaction-request/hooks/use-transaction-error.ts @@ -4,7 +4,12 @@ import { TransactionTypes } from '@stacks/connect'; import BigNumber from 'bignumber.js'; import { useFormikContext } from 'formik'; -import { useGetContractInterfaceQuery, useStxCryptoAssetBalance } from '@leather.io/query'; +import { + useCalculateStacksTxFees, + useGetContractInterfaceQuery, + useNextNonce, + useStxCryptoAssetBalance, +} from '@leather.io/query'; import { stxToMicroStx } from '@leather.io/utils'; import { StacksTransactionFormValues } from '@shared/models/form.model'; @@ -13,8 +18,13 @@ import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-s import { initialSearchParams } from '@app/common/initial-search-params'; import { validateStacksAddress } from '@app/common/stacks-utils'; import { TransactionErrorReason } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; -import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useCheckSbtcSponsorshipEligible } from '@app/query/sbtc/sponsored-transactions.hooks'; +import { + useCurrentStacksAccount, + useCurrentStacksAccountAddress, +} 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'; function getIsMultisig() { return initialSearchParams.get('isMultisig') === 'true'; @@ -30,10 +40,17 @@ export function useTransactionError() { const { filteredBalanceQuery } = useStxCryptoAssetBalance(currentAccount?.address ?? ''); 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); + return useMemo(() => { if (!origin) return TransactionErrorReason.ExpiredRequest; - if (filteredBalanceQuery.isLoading) return; + if (filteredBalanceQuery.isLoading || isVerifyingSbtcEligibilty) return; if (!transactionRequest || !availableUnlockedBalance || !currentAccount) { return TransactionErrorReason.Generic; @@ -56,7 +73,7 @@ export function useTransactionError() { return TransactionErrorReason.StxTransferInsufficientFunds; } - if (!transactionRequest.sponsored) { + if (!transactionRequest.sponsored && !sbtcSponsorshipEligibility?.isEligible) { if (zeroBalance) return TransactionErrorReason.FeeInsufficientFunds; const feeValue = stxToMicroStx(values.fee); @@ -73,5 +90,7 @@ export function useTransactionError() { availableUnlockedBalance, currentAccount, values.fee, + isVerifyingSbtcEligibilty, + sbtcSponsorshipEligibility, ]); } diff --git a/src/app/pages/swap/bitflow-swap-container.tsx b/src/app/pages/swap/bitflow-swap-container.tsx index c6948aa40e..4fa7c6c958 100644 --- a/src/app/pages/swap/bitflow-swap-container.tsx +++ b/src/app/pages/swap/bitflow-swap-container.tsx @@ -20,7 +20,10 @@ import { bitflow } from '@shared/utils/bitflow-sdk'; import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { Content, Page } from '@app/components/layout'; import { PageHeader } from '@app/features/container/headers/page.header'; -import type { TransactionBase } from '@app/query/sbtc/sponsored-transactions.query'; +import { + type SbtcSponsorshipEligibility, + type TransactionBase, +} from '@app/query/sbtc/sponsored-transactions.query'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useGenerateStacksContractCallUnsignedTx } from '@app/store/transactions/contract-call.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; @@ -50,8 +53,11 @@ function BitflowSwapContainer() { const broadcastStacksSwap = useStacksBroadcastSwap(); const { onDepositSbtc, onReviewDepositSbtc } = useSbtcDepositTransaction(); - const { isEligibleForSponsor, checkEligibilityForSponsor, submitSponsoredTx } = - useSponsorTransactionFees(); + const [sponsorshipEligibility, setSponsorshipEligibility] = useState< + SbtcSponsorshipEligibility | undefined + >(); + + const { checkEligibilityForSponsor, submitSponsoredTx } = useSponsorTransactionFees(); const { fetchRouteQuote, @@ -102,12 +108,10 @@ function BitflowSwapContainer() { const stacksSwapData = getStacksSwapSubmissionData({ bitflowSwapAssets, - isEligibleForSponsor, routeQuote, slippage, values, }); - onSetSwapSubmissionData(stacksSwapData); const swapExecutionData = { route: routeQuote.route, @@ -148,8 +152,12 @@ function BitflowSwapContainer() { if (!unsignedTx) return logger.error('Attempted to generate unsigned tx, but tx is undefined'); + const sponsorshipEligibility = await checkEligibilityForSponsor(values, unsignedTx); + stacksSwapData.sponsored = sponsorshipEligibility.isEligible; + setUnsignedTx(unsignedTx); - checkEligibilityForSponsor(values, unsignedTx); + setSponsorshipEligibility(sponsorshipEligibility); + onSetSwapSubmissionData(stacksSwapData); swapNavigate(RouteUrls.SwapReview); } finally { @@ -163,7 +171,6 @@ function BitflowSwapContainer() { fetchRouteQuote, generateUnsignedTx, isCrossChainSwap, - isEligibleForSponsor, onReviewDepositSbtc, onSetSwapSubmissionData, slippage, @@ -194,7 +201,8 @@ function BitflowSwapContainer() { } try { - if (isEligibleForSponsor) return await submitSponsoredTx(); + if (sponsorshipEligibility?.isEligible) + return await submitSponsoredTx(sponsorshipEligibility.unsignedSponsoredTx!); if (!unsignedTx?.transaction) return logger.error('No unsigned tx to sign'); @@ -214,10 +222,10 @@ function BitflowSwapContainer() { setIsIdle(); } }, [ + sponsorshipEligibility, broadcastStacksSwap, currentAccount, isCrossChainSwap, - isEligibleForSponsor, isLoading, navigate, onDepositSbtc, diff --git a/src/app/pages/swap/bitflow-swap.utils.ts b/src/app/pages/swap/bitflow-swap.utils.ts index bebc79ce55..94846246ff 100644 --- a/src/app/pages/swap/bitflow-swap.utils.ts +++ b/src/app/pages/swap/bitflow-swap.utils.ts @@ -20,14 +20,12 @@ function formatDexPathItem(dex: string) { interface getStacksSwapSubmissionDataArgs { bitflowSwapAssets: SwapAsset[]; - isEligibleForSponsor: boolean; routeQuote: RouteQuote; slippage: number; values: SwapFormValues; } export function getStacksSwapSubmissionData({ bitflowSwapAssets, - isEligibleForSponsor, routeQuote, slippage, values, @@ -44,7 +42,6 @@ export function getStacksSwapSubmissionData({ .map(x => bitflowSwapAssets.find(asset => asset.tokenId === x)) .filter(isDefined), slippage, - sponsored: isEligibleForSponsor, swapAmountBase: values.swapAmountBase, swapAmountQuote: values.swapAmountQuote, swapAssetBase: values.swapAssetBase, diff --git a/src/app/pages/swap/hooks/use-sponsor-tx-fees.tsx b/src/app/pages/swap/hooks/use-sponsor-tx-fees.tsx index cdfc6cd07e..a714c6bcf5 100644 --- a/src/app/pages/swap/hooks/use-sponsor-tx-fees.tsx +++ b/src/app/pages/swap/hooks/use-sponsor-tx-fees.tsx @@ -1,7 +1,10 @@ -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useStxCryptoAssetBalance } from '@leather.io/query'; +import type { StacksTransaction } from '@stacks/transactions'; + +import { FeeTypes } from '@leather.io/models'; +import { defaultFeesMaxValuesAsMoney } from '@leather.io/query'; import { logger } from '@shared/logger'; import type { SwapFormValues } from '@shared/models/form.model'; @@ -11,98 +14,56 @@ import { LoadingKeys, useLoading } from '@app/common/hooks/use-loading'; import { useToast } from '@app/features/toasts/use-toast'; import { useConfigSbtc } from '@app/query/common/remote-config/remote-config.query'; import { - type SbtcSponsorshipEligibility, type TransactionBase, submitSponsoredSbtcTransaction, verifySponsoredSbtcTransaction, } from '@app/query/sbtc/sponsored-transactions.query'; -import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; import { useSignStacksTransaction } from '@app/store/transactions/transaction.hooks'; -// TODO: Check sBTC balance is able to pay fee amount export function useSponsorTransactionFees() { - const [verificationResult, setVerificationResult] = useState< - SbtcSponsorshipEligibility | undefined - >(); - const [isEligibleForSponsor, setIsEligibleForSponsor] = useState(false); - const { isSbtcSwapsEnabled } = useConfigSbtc(); - const stxAddress = useCurrentStacksAccountAddress(); - const { filteredBalanceQuery } = useStxCryptoAssetBalance(stxAddress); + const { isSbtcSwapsEnabled, sponsorshipApiUrl } = useConfigSbtc(); const { setIsIdle } = useLoading(LoadingKeys.SUBMIT_SWAP_TRANSACTION); const signTx = useSignStacksTransaction(); const navigate = useNavigate(); const toast = useToast(); - const checkEligibilityForSponsor = useCallback( - (values: SwapFormValues, baseTx: TransactionBase) => { - const isSwapAssetBaseSbtc = values.swapAssetBase?.name === 'sBTC'; - const isSwapAssetQuoteSbtc = values.swapAssetQuote?.name === 'sBTC'; - const isSbtcBeingSwappedAndEligible = - (isSwapAssetBaseSbtc && values.swapAssetBase?.balance.amount.isGreaterThan(0)) || - (isSwapAssetQuoteSbtc && values.swapAssetQuote?.balance.amount.isGreaterThan(0)); + const checkEligibilityForSponsor = async (values: SwapFormValues, baseTx: TransactionBase) => { + if (!isSbtcSwapsEnabled) { + return { isEligible: false }; + } + return await verifySponsoredSbtcTransaction({ + apiUrl: sponsorshipApiUrl, + baseTx, + nonce: Number(values.nonce), + fee: defaultFeesMaxValuesAsMoney[FeeTypes.Middle].amount.toNumber(), + }); + }; - const isZeroStxBalance = filteredBalanceQuery.data?.availableBalance.amount.isEqualTo(0); + const submitSponsoredTx = useCallback( + async (unsignedSponsoredTx: StacksTransaction) => { + if (isSbtcSwapsEnabled) { + try { + const signedSponsoredTx = await signTx(unsignedSponsoredTx); + if (!signedSponsoredTx) return logger.error('Unable to sign sponsored transaction!'); - verifySponsoredSbtcTransaction({ - baseTx, - nonce: Number(values.nonce), - }) - .then(result => { - setVerificationResult(result); - }) - .catch(e => { - logger.error('Verification failure: ', e); - setVerificationResult({ isEligible: false }); - }) - .finally(() => { - setIsIdle; - }); + const result = await submitSponsoredSbtcTransaction(sponsorshipApiUrl, signedSponsoredTx); + if (!result.txid) { + navigate(RouteUrls.SwapError, { state: { message: result.error } }); + return; + } - setIsEligibleForSponsor( - !!( - verificationResult?.isEligible && - isZeroStxBalance && - isSbtcSwapsEnabled && - isSbtcBeingSwappedAndEligible - ) - ); + toast.success('Transaction submitted!'); + setIsIdle(); + navigate(RouteUrls.Activity); + } catch (error) { + return logger.error('Failed to submit sponsor transaction', error); + } + } }, - [ - filteredBalanceQuery.data?.availableBalance.amount, - isSbtcSwapsEnabled, - setIsIdle, - verificationResult?.isEligible, - ] + [navigate, setIsIdle, signTx, toast, isSbtcSwapsEnabled, sponsorshipApiUrl] ); - const submitSponsoredTx = useCallback(async () => { - if (isEligibleForSponsor) { - try { - const signedSponsoredTx = await signTx(verificationResult?.unsignedSponsoredTx!); - if (!signedSponsoredTx) return logger.error('Unable to sign sponsored transaction!'); - - const result = await submitSponsoredSbtcTransaction(signedSponsoredTx); - if (!result.txId) - navigate(RouteUrls.TransactionBroadcastError, { state: { message: result.error } }); - - toast.success('Transaction submitted!'); - setIsIdle(); - navigate(RouteUrls.Activity); - } catch (error) { - return logger.error('Failed to submit sponsor transaction', error); - } - } - }, [ - isEligibleForSponsor, - navigate, - setIsIdle, - signTx, - toast, - verificationResult?.unsignedSponsoredTx, - ]); - return { - isEligibleForSponsor, checkEligibilityForSponsor, submitSponsoredTx, }; diff --git a/src/app/pages/transaction-request/transaction-request.tsx b/src/app/pages/transaction-request/transaction-request.tsx index 797d4e99cf..f910f912e3 100644 --- a/src/app/pages/transaction-request/transaction-request.tsx +++ b/src/app/pages/transaction-request/transaction-request.tsx @@ -15,12 +15,15 @@ import { import { Link } from '@leather.io/ui'; import { isString } from '@leather.io/utils'; +import { finalizeTxSignature } from '@shared/actions/finalize-tx-signature'; import { logger } from '@shared/logger'; import { StacksTransactionFormValues } from '@shared/models/form.model'; import { RouteUrls } from '@shared/route-urls'; import { analytics } from '@shared/utils/analytics'; +import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params'; import { useOnMount } from '@app/common/hooks/use-on-mount'; +import { stacksTransactionToHex } from '@app/common/transactions/stacks/transaction.utils'; import { stxFeeValidator } from '@app/common/validation/forms/fee-validators'; import { nonceValidator } from '@app/common/validation/nonce-validators'; import { NonceSetter } from '@app/components/nonce-setter'; @@ -38,10 +41,14 @@ import { PostConditions } from '@app/features/stacks-transaction-request/post-co import { StxTransferDetails } from '@app/features/stacks-transaction-request/stx-transfer-details/stx-transfer-details'; import { StacksTxSubmitAction } from '@app/features/stacks-transaction-request/submit-action'; import { TransactionError } from '@app/features/stacks-transaction-request/transaction-error/transaction-error'; +import { useConfigSbtc } from '@app/query/common/remote-config/remote-config.query'; import { useCheckSbtcSponsorshipEligible } from '@app/query/sbtc/sponsored-transactions.hooks'; import { submitSponsoredSbtcTransaction } from '@app/query/sbtc/sponsored-transactions.query'; import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; -import { useTransactionRequestState } from '@app/store/transactions/requests.hooks'; +import { + useTransactionRequest, + useTransactionRequestState, +} from '@app/store/transactions/requests.hooks'; import { useGenerateUnsignedStacksTransaction, useSignStacksTransaction, @@ -49,6 +56,10 @@ import { } from '@app/store/transactions/transaction.hooks'; function TransactionRequestBase() { + const sbtcConfig = useConfigSbtc(); + const { tabId } = useDefaultRequestParams(); + const requestToken = useTransactionRequest(); + const transactionRequest = useTransactionRequestState(); const unsignedTx = useUnsignedStacksTransactionBaseState(); const { data: stxFees } = useCalculateStacksTxFees(unsignedTx.transaction); @@ -62,7 +73,7 @@ function TransactionRequestBase() { const { data: nextNonce, status: nonceQueryStatus } = useNextNonce(stxAddress); const { isVerifying: isVerifyingSbtcSponsorship, result: sbtcSponsorshipEligibility } = - useCheckSbtcSponsorshipEligible(unsignedTx, nextNonce?.nonce, stxFees); + useCheckSbtcSponsorshipEligible(unsignedTx, nextNonce, stxFees); const canSubmit = filteredBalanceQuery.status === 'success' && @@ -86,9 +97,24 @@ function TransactionRequestBase() { sbtcSponsorshipEligibility.unsignedSponsoredTx! ); if (!signedSponsoredTx) throw new Error('Unable to sign sponsored transaction!'); - const result = await submitSponsoredSbtcTransaction(signedSponsoredTx); - if (!result.txId) + const result = await submitSponsoredSbtcTransaction( + sbtcConfig.sponsorshipApiUrl, + signedSponsoredTx + ); + if (!result.txid) { navigate(RouteUrls.TransactionBroadcastError, { state: { message: result.error } }); + return; + } + if (requestToken && tabId) { + finalizeTxSignature({ + requestPayload: requestToken, + tabId: tabId, + data: { + txRaw: stacksTransactionToHex(signedSponsoredTx), + txId: result.txid, + }, + }); + } } catch (e: any) { const message = isString(e) ? e : e.message; navigate(RouteUrls.TransactionBroadcastError, { state: { message } }); diff --git a/src/app/query/common/remote-config/remote-config.query.ts b/src/app/query/common/remote-config/remote-config.query.ts index a5f66c5b63..f7658aba47 100644 --- a/src/app/query/common/remote-config/remote-config.query.ts +++ b/src/app/query/common/remote-config/remote-config.query.ts @@ -8,16 +8,16 @@ import { useHasCurrentBitcoinAccount } from '@app/store/accounts/blockchain/bitc import { useCurrentNetwork } from '@app/store/networks/networks.selectors'; export { - HiroMessage, - AvailableRegions, ActiveFiatProvider, - useRemoteLeatherMessages, + AvailableRegions, + HiroMessage, useActiveFiatProviders, - useHasFiatProviders, - useRecoverUninscribedTaprootUtxosFeatureEnabled, useConfigNftMetadataEnabled, useConfigRunesEnabled, useConfigSwapsEnabled, + useHasFiatProviders, + useRecoverUninscribedTaprootUtxosFeatureEnabled, + useRemoteLeatherMessages, } from '@leather.io/query'; export function useConfigBitcoinEnabled() { @@ -45,8 +45,12 @@ interface SbtcConfig { enabled: boolean; contracts: Record<'mainnet' | 'testnet', { address: string }>; emilyApiUrl: string; - leatherSponsorApiUrl: string; + sponsorshipApiUrl: { + mainnet: string; + testnet: string; + }; swapsEnabled: boolean; + sponsorshipsEnabled: boolean; } export function useConfigSbtc() { @@ -58,13 +62,17 @@ export function useConfigSbtc() { const displayPromoCardOnNetworks = (sbtc as any)?.showPromoLinkOnNetworks ?? []; const contractIdMainnet = sbtc?.contracts.mainnet.address ?? ''; const contractIdTestnet = sbtc?.contracts.testnet.address ?? ''; + const apiUrlMainnet = sbtc?.sponsorshipApiUrl.mainnet ?? ''; + const apiUrlTestnet = sbtc?.sponsorshipApiUrl.testnet ?? ''; return { + configLoading: !sbtc, isSbtcEnabled: sbtc?.enabled ?? false, isSbtcSwapsEnabled: (sbtc?.enabled && sbtc?.swapsEnabled) ?? false, + isSbtcSponsorshipsEnabled: (sbtc?.enabled && sbtc?.sponsorshipsEnabled) ?? false, emilyApiUrl: sbtc?.emilyApiUrl ?? '', - leatherSponsorApiUrl: sbtc?.leatherSponsorApiUrl ?? '', contractId: network.chain.bitcoin.mode === 'mainnet' ? contractIdMainnet : contractIdTestnet, + sponsorshipApiUrl: network.chain.bitcoin.mode === 'mainnet' ? apiUrlMainnet : apiUrlTestnet, isSbtcContract(contract: string) { return ( contract === getPrincipalFromContractId(contractIdMainnet) || diff --git a/src/app/query/sbtc/sponsored-transactions.hooks.ts b/src/app/query/sbtc/sponsored-transactions.hooks.ts index 88ca9f3f4b..3a6335806a 100644 --- a/src/app/query/sbtc/sponsored-transactions.hooks.ts +++ b/src/app/query/sbtc/sponsored-transactions.hooks.ts @@ -1,11 +1,13 @@ import { useEffect, useState } from 'react'; -import type { Fees } from '@leather.io/models'; +import { FeeTypes, type Fees } from '@leather.io/models'; +import type { NextNonce } from '@leather.io/query'; import { logger } from '@shared/logger'; import { useCurrentStacksAccountAddress } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; +import { useConfigSbtc } from '../common/remote-config/remote-config.query'; import { type SbtcSponsorshipEligibility, type SbtcSponsorshipVerificationResult, @@ -15,22 +17,34 @@ import { export function useCheckSbtcSponsorshipEligible( baseTx?: TransactionBase, - nonce?: number, + nextNonce?: NextNonce, stxFees?: Fees ): SbtcSponsorshipVerificationResult { + const sbtcConfig = useConfigSbtc(); + const stxAddress = useCurrentStacksAccountAddress(); const [isLoading, setIsLoading] = useState(true); const [result, setResult] = useState(); - const stxAddress = useCurrentStacksAccountAddress(); const [lastAddressChecked, setLastAddressChecked] = useState(); useEffect(() => { - if (!(baseTx && nonce && stxFees)) { + if (!sbtcConfig.configLoading && !sbtcConfig.isSbtcSponsorshipsEnabled) { + if (isLoading) setIsLoading(false); + return; + } + if (!(sbtcConfig && baseTx && nextNonce && stxFees)) { return; } if (result && stxAddress === lastAddressChecked) { return; } - verifySponsoredSbtcTransaction({ baseTx, nonce, stxFees }) + // use the standard recommended fee from estimates + const standardFeeEstimate = stxFees.estimates[FeeTypes.Middle].fee.amount.toNumber(); + verifySponsoredSbtcTransaction({ + apiUrl: sbtcConfig.sponsorshipApiUrl, + baseTx, + nonce: nextNonce.nonce, + fee: standardFeeEstimate, + }) .then(result => { setResult(result); setLastAddressChecked(stxAddress); @@ -42,7 +56,7 @@ export function useCheckSbtcSponsorshipEligible( .finally(() => { setIsLoading(false); }); - }, [baseTx, stxFees, result, stxAddress, lastAddressChecked, nonce]); + }, [baseTx, stxFees, result, stxAddress, lastAddressChecked, nextNonce, isLoading, sbtcConfig]); return { isVerifying: isLoading, diff --git a/src/app/query/sbtc/sponsored-transactions.query.ts b/src/app/query/sbtc/sponsored-transactions.query.ts index 954b9331ce..1010ff607d 100644 --- a/src/app/query/sbtc/sponsored-transactions.query.ts +++ b/src/app/query/sbtc/sponsored-transactions.query.ts @@ -2,11 +2,9 @@ import { bytesToHex } from '@stacks/common'; import { StacksTransaction } from '@stacks/transactions'; import axios from 'axios'; -import { FeeTypes, type Fees } from '@leather.io/models'; -import { defaultStacksFees } from '@leather.io/query'; - import { logger } from '@shared/logger'; +import { queryClient } from '@app/common/persistence'; import { generateUnsignedTransaction } from '@app/common/transactions/stacks/generate-unsigned-txs'; export interface TransactionBase { @@ -25,21 +23,20 @@ export interface SbtcSponsorshipEligibility { } interface SbtcSponsorshipSubmissionResult { - txId?: string; + txid?: string; error?: string; } export async function submitSponsoredSbtcTransaction( + apiUrl: string, sponsoredTx: StacksTransaction ): Promise { - logger.debug('Submitting Sponsored sBTC Transaction!'); try { - const { data } = await axios.post('http://localhost:5001/submit', { + const { data } = await axios.post(`${apiUrl}/submit`, { tx: bytesToHex(sponsoredTx.serialize()), }); - logger.debug('sBTC Sponsorship Success:', data.txId); return { - txId: data.txId, + txid: data.txid, }; } catch (error: any) { const errMsg = `sBTC Sponsorship Failure (${error?.response?.data?.error || 'Unknown'})`; @@ -50,42 +47,43 @@ export async function submitSponsoredSbtcTransaction( } interface VerifySponsoredSbtcTransactionArgs { + apiUrl: string; baseTx: TransactionBase; nonce?: number; - stxFees?: Fees; + fee?: number; } export async function verifySponsoredSbtcTransaction({ + apiUrl, baseTx, nonce, - stxFees, + fee, }: VerifySponsoredSbtcTransactionArgs): Promise { - logger.debug('Verifying Sponsored sBTC Transaction!'); try { - // use the standard recommended fee - const standardFee = - stxFees?.estimates[FeeTypes.Middle].fee.amount.toNumber() ?? - defaultStacksFees.estimates[FeeTypes.Middle].fee.amount.toNumber(); // add sponsorship option const { options } = baseTx as any; options.txData.sponsored = true; const sponsoredTx = await generateUnsignedTransaction({ ...options, - fee: standardFee, + fee, nonce, }); const serializedTx = bytesToHex(sponsoredTx.serialize()); - logger.debug('Pre-serialization:', { - fee: sponsoredTx.auth.spendingCondition?.fee, - authType: sponsoredTx.auth.authType, - rawTx: serializedTx, - }); - const { data } = await axios.post('http://localhost:5001/verify', { - tx: serializedTx, + const result = await queryClient.fetchQuery({ + queryKey: ['verify-sponsored-sbtc-transaction', serializedTx], + queryFn: async () => { + const { data } = await axios.post( + `${apiUrl}/verify`, + { + tx: serializedTx, + }, + { timeout: 5000 } + ); + return data; + }, }); - logger.debug('Verification response:', data); - return { isEligible: true, unsignedSponsoredTx: sponsoredTx }; + return { isEligible: result, unsignedSponsoredTx: sponsoredTx }; } catch (error) { logger.error('Transaction verification failed:', error); return { isEligible: false };