Skip to content

Commit

Permalink
refactor: sbtc sponsorships cleanup and adjustments
Browse files Browse the repository at this point in the history
  • Loading branch information
alexp3y committed Dec 17, 2024
1 parent 0417c9b commit 59b905c
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 137 deletions.
6 changes: 5 additions & 1 deletion config/wallet-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
19 changes: 16 additions & 3 deletions config/wallet-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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<TransactionErrorReason | void>(() => {
if (!origin) return TransactionErrorReason.ExpiredRequest;

if (filteredBalanceQuery.isLoading) return;
if (filteredBalanceQuery.isLoading || isVerifyingSbtcEligibilty) return;

if (!transactionRequest || !availableUnlockedBalance || !currentAccount) {
return TransactionErrorReason.Generic;
Expand All @@ -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);
Expand All @@ -73,5 +90,7 @@ export function useTransactionError() {
availableUnlockedBalance,
currentAccount,
values.fee,
isVerifyingSbtcEligibilty,
sbtcSponsorshipEligibility,
]);
}
26 changes: 17 additions & 9 deletions src/app/pages/swap/bitflow-swap-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -102,12 +108,10 @@ function BitflowSwapContainer() {

const stacksSwapData = getStacksSwapSubmissionData({
bitflowSwapAssets,
isEligibleForSponsor,
routeQuote,
slippage,
values,
});
onSetSwapSubmissionData(stacksSwapData);

const swapExecutionData = {
route: routeQuote.route,
Expand Down Expand Up @@ -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 {
Expand All @@ -163,7 +171,6 @@ function BitflowSwapContainer() {
fetchRouteQuote,
generateUnsignedTx,
isCrossChainSwap,
isEligibleForSponsor,
onReviewDepositSbtc,
onSetSwapSubmissionData,
slippage,
Expand Down Expand Up @@ -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');

Expand All @@ -214,10 +222,10 @@ function BitflowSwapContainer() {
setIsIdle();
}
}, [
sponsorshipEligibility,
broadcastStacksSwap,
currentAccount,
isCrossChainSwap,
isEligibleForSponsor,
isLoading,
navigate,
onDepositSbtc,
Expand Down
3 changes: 0 additions & 3 deletions src/app/pages/swap/bitflow-swap.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
111 changes: 36 additions & 75 deletions src/app/pages/swap/hooks/use-sponsor-tx-fees.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
};
Expand Down
Loading

0 comments on commit 59b905c

Please sign in to comment.