From 3dab4fff35c629a75d8146e0394698bc81fadef4 Mon Sep 17 00:00:00 2001 From: evandrosaturnino Date: Fri, 31 Oct 2025 00:19:10 -0500 Subject: [PATCH] feat: add gasless minting support --- src/hooks/tbtc/useDepositTelemetry.ts | 24 ++- .../tBTC/Deposit/Minting/InitiateMinting.tsx | 6 +- .../tBTC/Deposit/Minting/MakeDeposit.tsx | 30 +++- src/threshold-ts/tbtc/constants.ts | 25 +++ src/threshold-ts/tbtc/index.ts | 144 ++++++++++++++++++ src/utils/isLocalhost.ts | 17 +++ 6 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 src/threshold-ts/tbtc/constants.ts create mode 100644 src/utils/isLocalhost.ts diff --git a/src/hooks/tbtc/useDepositTelemetry.ts b/src/hooks/tbtc/useDepositTelemetry.ts index 292de809b..727e02451 100644 --- a/src/hooks/tbtc/useDepositTelemetry.ts +++ b/src/hooks/tbtc/useDepositTelemetry.ts @@ -5,6 +5,7 @@ import { BitcoinNetwork } from "../../threshold-ts/types" import { verifyDepositAddress } from "../../utils/verifyDepositAddress" import { useCaptureMessage } from "../sentry" import { ApiUrl, endpointUrl } from "../../enums" +import { isLocalhost } from "../../utils/isLocalhost" export const useDepositTelemetry = (network: BitcoinNetwork) => { const captureMessage = useCaptureMessage() @@ -43,6 +44,14 @@ export const useDepositTelemetry = (network: BitcoinNetwork) => { } ) + // Skip telemetry submission in localhost to avoid CORS issues + if (isLocalhost()) { + console.info( + "Skipping deposit telemetry submission in localhost environment" + ) + return + } + const requestBody = { depositAddress, depositor: depositor.identifierHex, @@ -61,8 +70,19 @@ export const useDepositTelemetry = (network: BitcoinNetwork) => { requestBody, { timeout: 10000 } ) - } catch (error) { - throw new Error("Failed to submit deposit telemetry", { cause: error }) + } catch (error: any) { + // Log the error but don't throw it to prevent blocking the deposit flow + console.warn("Failed to submit deposit telemetry:", error.message) + + // In production, throw only for non-CORS errors + if ( + !isLocalhost() && + (error.response || error.code !== "ERR_NETWORK") + ) { + throw new Error("Failed to submit deposit telemetry", { + cause: error, + }) + } } }, [verifyDepositAddress, network, captureMessage] diff --git a/src/pages/tBTC/Deposit/Minting/InitiateMinting.tsx b/src/pages/tBTC/Deposit/Minting/InitiateMinting.tsx index 19a745a04..0a7837655 100644 --- a/src/pages/tBTC/Deposit/Minting/InitiateMinting.tsx +++ b/src/pages/tBTC/Deposit/Minting/InitiateMinting.tsx @@ -87,7 +87,7 @@ const InitiateMintingComponent: FC<{ withSymbol />{" "} and will receive{" "} - + {tBTCMintAmount ? ( - + ) : ( + -- + )} {chainInfo.mintingProcessDescription} diff --git a/src/pages/tBTC/Deposit/Minting/MakeDeposit.tsx b/src/pages/tBTC/Deposit/Minting/MakeDeposit.tsx index 683f0dc92..d63e050bf 100644 --- a/src/pages/tBTC/Deposit/Minting/MakeDeposit.tsx +++ b/src/pages/tBTC/Deposit/Minting/MakeDeposit.tsx @@ -108,19 +108,33 @@ const BTCAddressSection: FC<{ maxW="205px" borderRadius="8px" > - + {btcDepositAddress ? ( + + ) : ( + + Loading address... + + )} - + - {btcDepositAddress} + {btcDepositAddress || "..."} { + return isTestnet ? TBTC_API_ENDPOINTS.TESTNET : TBTC_API_ENDPOINTS.MAINNET +} diff --git a/src/threshold-ts/tbtc/index.ts b/src/threshold-ts/tbtc/index.ts index 184475081..02b7532b4 100644 --- a/src/threshold-ts/tbtc/index.ts +++ b/src/threshold-ts/tbtc/index.ts @@ -1,5 +1,6 @@ import { BlockTag, TransactionReceipt } from "@ethersproject/abstract-provider" import { Web3Provider, JsonRpcProvider } from "@ethersproject/providers" +import axios from "axios" import { BitcoinClient, BitcoinTx, @@ -9,12 +10,16 @@ import { CrossChainDepositor, Deposit, DepositRequest, + DepositReceipt, + BitcoinRawTxVectors, ElectrumClient, ethereumAddressFromSigner, EthereumBridge, chainIdFromSigner, Hex, loadEthereumCoreContracts, + packRevealDepositParameters, + extractBitcoinRawTxVectors, TBTC as SDK, Chains, DestinationChainName, @@ -66,6 +71,7 @@ import { SupportedChainIds } from "../../networks/enums/networks" import { getThresholdLibProvider } from "../../utils/getThresholdLib" import { getEthereumDefaultProviderChainId } from "../../utils/getEnvVariable" import { getCrossChainRpcUrl } from "../../networks/utils/getCrossChainRpcUrl" +import { getApiEndpoints } from "./constants" export enum BridgeActivityStatus { PENDING = "PENDING", @@ -352,6 +358,22 @@ export interface ITBTC { */ revealDeposit(utxo: BitcoinUtxo): Promise + /** + * Reveals the given deposit using gasless reveal through a relayer service. + * This method calls an external API to trigger the deposit transaction via a relayer. + * @param depositTx Bitcoin raw transaction vectors + * @param depositOutputIndex Index of the deposit output + * @param deposit Deposit receipt containing the deposit parameters + * @param vault Optional vault address + * @returns Transaction receipt from the gasless reveal + */ + gaslessRevealDeposit( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise + /** * Gets a revealed deposit from the bridge. * @param utxo Deposit UTXO of the revealed deposit @@ -1107,6 +1129,46 @@ export class TBTC implements ITBTC { const { value, ...transactionOutpoint } = utxo if (!this._deposit) throw new EmptyDepositObjectError() + // Check if we should use gasless reveal for L1 networks + const isL1 = + !this._crossChainConfig.isCrossChain && this._ethereumConfig.chainId + if (isL1) { + // Use gasless reveal for L1 networks + const depositReceipt = this._deposit.getReceipt() + + // Get the raw bitcoin transaction + const rawTx = await this._bitcoinClient.getRawTransaction( + utxo.transactionHash + ) + + // Extract transaction vectors + const depositTx = extractBitcoinRawTxVectors(rawTx) + + // Get vault if available + const vaultAddress = this._tbtcVaultContract?.address + let vault: ChainIdentifier | undefined + if (vaultAddress) { + // Create a proper ChainIdentifier object + vault = { + identifierHex: vaultAddress.slice(2).toLowerCase(), + equals: function (other: ChainIdentifier): boolean { + return this.identifierHex === other.identifierHex + }, + } + } + + const receipt = await this.gaslessRevealDeposit( + depositTx, + utxo.outputIndex, + depositReceipt, + vault + ) + + this.removeDepositData() + return receipt + } + + // Use regular reveal for L2/cross-chain networks const result = await this._deposit.initiateMinting(transactionOutpoint) this.removeDepositData() @@ -1125,6 +1187,88 @@ export class TBTC implements ITBTC { throw new Error("Unexpected result type from initiateMinting") } + /** + * Reveals the given deposit using gasless reveal through a relayer service. + * This method calls an external API to trigger the deposit transaction via a relayer. + * @param {BitcoinRawTxVectors} depositTx Bitcoin raw transaction vectors + * @param {number} depositOutputIndex Index of the deposit output + * @param {DepositReceipt} deposit Deposit receipt containing the deposit parameters + * @param {ChainIdentifier} vault Optional vault address + * @return {Promise} Transaction receipt from the gasless reveal + */ + gaslessRevealDeposit = async ( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise => { + const { fundingTx, reveal } = packRevealDepositParameters( + depositTx, + depositOutputIndex, + deposit, + vault + ) + + // For gasless reveals, we need the deposit owner address + const depositOwner = deposit.depositor + + // Determine the API endpoint based on network + const isTestnet = this.bitcoinNetwork === BitcoinNetwork.Testnet + const apiEndpoints = getApiEndpoints(isTestnet) + const apiUrl = apiEndpoints.GASLESS_REVEAL + + try { + const response = await axios.post(apiUrl, { + fundingTx, + reveal, + destinationChainDepositOwner: `0x${depositOwner.identifierHex}`, + }) + + const { data } = response + if (!data || data.status !== "success") { + throw new Error( + `Unexpected response from /api/gasless-reveal: ${JSON.stringify( + data + )}` + ) + } + + // Convert the response to match TransactionReceipt format + const receipt: TransactionReceipt = { + to: data.data.contractAddress || "", + from: "", + contractAddress: data.data.contractAddress || "", + transactionIndex: 0, + root: undefined, + gasUsed: BigNumber.from(0), + logsBloom: "", + blockHash: "", + transactionHash: data.data.transactionHash, + logs: [], + blockNumber: data.data.blockNumber, + confirmations: 0, + cumulativeGasUsed: BigNumber.from(0), + effectiveGasPrice: BigNumber.from(0), + byzantium: true, + type: 0, + status: data.data.status, + } + + return receipt + } catch (error: any) { + if (error.response) { + console.error("Gasless reveal API error response:", error.response.data) + throw new Error( + error.response.data?.message || + error.response.data?.error || + "Failed to perform gasless reveal" + ) + } + console.error("Error calling /api/gasless-reveal endpoint:", error) + throw error + } + } + getRevealedDeposit = async (utxo: BitcoinUtxo): Promise => { const sdk = await this._getSdk() const deposit = await sdk.tbtcContracts.bridge.deposits( diff --git a/src/utils/isLocalhost.ts b/src/utils/isLocalhost.ts new file mode 100644 index 000000000..95ca06698 --- /dev/null +++ b/src/utils/isLocalhost.ts @@ -0,0 +1,17 @@ +/** + * Check if the current environment is localhost/development + * @return {boolean} True if running on localhost + */ +export const isLocalhost = (): boolean => { + if (typeof window === "undefined") return false + + const hostname = window.location.hostname + return ( + hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "0.0.0.0" || + hostname.startsWith("192.168.") || + hostname.startsWith("10.") || + hostname.endsWith(".local") + ) +}