diff --git a/.env.example b/.env.example index 30c29f70..d8a93ef4 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ NEXT_PUBLIC_MEMPOOL_API=https://babylon.mempool.space -NEXT_PUBLIC_API_URL=https://staking-api.staging.babylonchain.io +NEXT_PUBLIC_API_URL=https://staking-api.testnet.babylonchain.io NEXT_PUBLIC_NETWORK=signet NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES=true \ No newline at end of file diff --git a/LICENSE b/LICENSE index ff9cc0e7..735df13e 100644 --- a/LICENSE +++ b/LICENSE @@ -8,10 +8,10 @@ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. Parameters -Licensor: Babylonchain, Inc. +Licensor: Babylon Labs, Ltd. Licensed Work: simple-staking - The Licensed Work is (c) 2024 Babylonchain, Inc. + The Licensed Work is (c) 2024 Babylon Labs, Ltd. Additional Use Grant: None. diff --git a/docs/WalletIntegration.md b/docs/WalletIntegration.md index 449b7efc..4c230c15 100644 --- a/docs/WalletIntegration.md +++ b/docs/WalletIntegration.md @@ -56,9 +56,11 @@ export interface UTXO { scriptPubKey: string; } -export interface Inscription { - // output of the inscription in the format of `txid:vout` - output: string; +export interface InscriptionIdentifier { + // hash of transaction that holds the ordinals/brc-2-/runes etc in the UTXO + txid: string; + // index of the output in the transaction + vout: number; } // supported networks @@ -342,25 +344,47 @@ export class OKXWallet extends WalletProvider { return await getTipHeight(); }; - // Inscriptions(Ordinal/Runes/BRC-20 etc) - getInscriptions = async (): Promise => { + getInscriptions = async (): Promise => { if (!this.okxWalletInfo) { throw new Error("OKX Wallet not connected"); } - const inscriptions: Inscription[] = []; + // max num of iterations to prevent infinite loop + const MAX_ITERATIONS = 100; + // Fetch inscriptions in batches of 100 + const limit = 100; + const inscriptionIdentifiers: InscriptionIdentifier[] = []; let cursor = 0; - while (true) { - const { list } = await this.bitcoinNetworkProvider.getInscriptions( - cursor, - DEFAULT_INSCRIPTION_LIMIT, - ); - inscriptions.push(...list); - if (list.length < DEFAULT_INSCRIPTION_LIMIT) { - break; + let iterations = 0; + try { + while (iterations < MAX_ITERATIONS) { + const { list } = await this.bitcoinNetworkProvider.getInscriptions( + cursor, + limit, + ); + const identifiers = list.map((i: { output: string }) => { + const [txid, vout] = i.output.split(":"); + return { + txid, + vout, + }; + }); + inscriptionIdentifiers.push(...identifiers); + if (list.length < limit) { + break; + } + cursor += limit; + iterations++; + if (iterations >= MAX_ITERATIONS) { + throw new Error( + "Exceeded maximum iterations when fetching inscriptions", + ); + } } - cursor += DEFAULT_INSCRIPTION_LIMIT; + } catch (error) { + throw new Error("Failed to get inscriptions from OKX Wallet"); } - return inscriptions; + + return inscriptionIdentifiers; }; } ``` diff --git a/package-lock.json b/package-lock.json index 0e363ef7..98b81b55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-staking", - "version": "0.2.19", + "version": "0.2.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-staking", - "version": "0.2.19", + "version": "0.2.23", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@keystonehq/animated-qr": "^0.8.6", diff --git a/package.json b/package.json index d033ad07..bdb3a274 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-staking", - "version": "0.2.20", + "version": "0.2.23", "private": true, "scripts": { "dev": "next dev", @@ -10,8 +10,8 @@ "format": "prettier --check .", "format:fix": "prettier --write .", "clean": "rm -r node_modules", - "build-docker": "docker build -t babylonchain/simple-staking .", - "clean-docker": "docker rmi babylonchain/simple-staking 2>/dev/null; true", + "build-docker": "docker build -t babylonlabs/simple-staking .", + "clean-docker": "docker rmi babylonlabs/simple-staking 2>/dev/null; true", "prepare": "husky", "sort-imports": "eslint --fix .", "test": "jest", diff --git a/src/app/api/error/index.ts b/src/app/api/error/index.ts new file mode 100644 index 00000000..8fc79bb7 --- /dev/null +++ b/src/app/api/error/index.ts @@ -0,0 +1,8 @@ +import { isAxiosError } from "axios"; + +export const isAxiosError451 = (error: any): boolean => { + return ( + isAxiosError(error) && + (error.response?.status === 451 || error.request.status === 451) + ); +}; diff --git a/src/app/api/healthCheckClient.ts b/src/app/api/healthCheckClient.ts new file mode 100644 index 00000000..8d80a660 --- /dev/null +++ b/src/app/api/healthCheckClient.ts @@ -0,0 +1,12 @@ +import axios from "axios"; + +interface HealthCheckResponse { + data: string; +} + +export const fetchHealthCheck = async (): Promise => { + const response = await axios.get( + `${process.env.NEXT_PUBLIC_API_URL}/healthcheck`, + ); + return response.data; +}; diff --git a/src/app/components/Connect/ConnectSmall.tsx b/src/app/components/Connect/ConnectSmall.tsx index e0a5deae..906e6b38 100644 --- a/src/app/components/Connect/ConnectSmall.tsx +++ b/src/app/components/Connect/ConnectSmall.tsx @@ -1,9 +1,12 @@ import { useRef, useState } from "react"; +import { AiOutlineInfoCircle } from "react-icons/ai"; import { FaBitcoin } from "react-icons/fa"; import { IoMdClose } from "react-icons/io"; import { PiWalletBold } from "react-icons/pi"; +import { Tooltip } from "react-tooltip"; import { useOnClickOutside } from "usehooks-ts"; +import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { getNetworkConfig } from "@/config/network.config"; import { satoshiToBtc } from "@/utils/btcConversions"; import { maxDecimals } from "@/utils/maxDecimals"; @@ -34,6 +37,27 @@ export const ConnectSmall: React.FC = ({ useOnClickOutside(ref, handleClickOutside); const { coinName, networkName } = getNetworkConfig(); + const { isApiNormal, isGeoBlocked, apiMessage } = useHealthCheck(); + + // Renders the Tooltip describing the reason + // why the user might not be able to connect the wallet + const renderApiNotAvailableTooltip = () => { + if (!isGeoBlocked && isApiNormal) return null; + + return ( + <> + + + + + + ); + }; return address ? (
@@ -92,13 +116,18 @@ export const ConnectSmall: React.FC = ({ )}
) : ( - +
+ + {!isApiNormal && renderApiNotAvailableTooltip()} +
); }; diff --git a/src/app/components/FAQ/data/questions.ts b/src/app/components/FAQ/data/questions.ts index 425c5487..877e1df0 100644 --- a/src/app/components/FAQ/data/questions.ts +++ b/src/app/components/FAQ/data/questions.ts @@ -60,7 +60,7 @@ export const questions = (coinName: string): Question[] => { }, { title: "Are there any other ways to stake?", - content: `

Hands-on stakers can operate the btc-staker CLI program that allows for the creation of ${coinName} staking transactions from the CLI.

+ content: `

Hands-on stakers can operate the btc-staker CLI program that allows for the creation of ${coinName} staking transactions from the CLI.

`, }, { diff --git a/src/app/components/Footer/Footer.tsx b/src/app/components/Footer/Footer.tsx index c7276443..44405029 100644 --- a/src/app/components/Footer/Footer.tsx +++ b/src/app/components/Footer/Footer.tsx @@ -25,7 +25,7 @@ const iconLinks = [ }, { name: "GitHub", - url: "https://github.com/babylonchain", + url: "https://github.com/babylonlabs-io", Icon: BsGithub, }, { @@ -50,7 +50,7 @@ const iconLinks = [ }, { name: "Email", - url: "mailto:contact@babylonchain.io", + url: "mailto:contact@babylonlabs.io", Icon: MdAlternateEmail, }, { diff --git a/src/app/components/Header/Header.tsx b/src/app/components/Header/Header.tsx index cdbf5ff6..f0761797 100644 --- a/src/app/components/Header/Header.tsx +++ b/src/app/components/Header/Header.tsx @@ -1,3 +1,5 @@ +import { shouldDisplayTestingMsg } from "@/config"; + import { ConnectSmall } from "../Connect/ConnectSmall"; import { ConnectedSmall } from "../Connect/ConnectedSmall"; import { TestingInfo } from "../TestingInfo/TestingInfo"; @@ -21,36 +23,38 @@ export const Header: React.FC = ({ return ( ); }; diff --git a/src/app/components/Modals/Terms/data/terms.tsx b/src/app/components/Modals/Terms/data/terms.tsx index b7b92af4..563f4957 100644 --- a/src/app/components/Modals/Terms/data/terms.tsx +++ b/src/app/components/Modals/Terms/data/terms.tsx @@ -83,8 +83,8 @@ export const Terms = () => {

If you have any dispute or claim arising out of or relating in any way to the Interface or these Terms, you must send an email to{" "} - - contracts@babylonchain.io + + contact@babylonlabs.io {" "} to resolve the matter via an informal, good faith negotiation process. If that dispute or claim is not resolved within 60 days of sending such diff --git a/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx b/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx index 73a8d012..791b1a2c 100644 --- a/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx +++ b/src/app/components/Staking/FinalityProviders/FinalityProviders.tsx @@ -32,7 +32,7 @@ export const FinalityProviders: React.FC = ({ } const network = getNetworkConfig().network; - const createFinalityProviderLink = `https://github.com/babylonchain/networks/tree/main/${ + const createFinalityProviderLink = `https://github.com/babylonlabs-io/networks/tree/main/${ network == Network.MAINNET ? "bbn-1" : "bbn-test-4" }/finality-providers`; return ( diff --git a/src/app/components/Staking/Form/States/api-not-available.svg b/src/app/components/Staking/Form/States/api-not-available.svg new file mode 100644 index 00000000..f964f25c --- /dev/null +++ b/src/app/components/Staking/Form/States/api-not-available.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/src/app/components/Staking/Form/States/geo-restricted.svg b/src/app/components/Staking/Form/States/geo-restricted.svg new file mode 100644 index 00000000..23f87fbf --- /dev/null +++ b/src/app/components/Staking/Form/States/geo-restricted.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/app/components/Staking/Staking.tsx b/src/app/components/Staking/Staking.tsx index 732b4974..4e3c709b 100644 --- a/src/app/components/Staking/Staking.tsx +++ b/src/app/components/Staking/Staking.tsx @@ -13,6 +13,7 @@ import { LoadingView } from "@/app/components/Loading/Loading"; import { useError } from "@/app/context/Error/ErrorContext"; import { useGlobalParams } from "@/app/context/api/GlobalParamsProvider"; import { useStakingStats } from "@/app/context/api/StakingStatsProvider"; +import { useHealthCheck } from "@/app/hooks/useHealthCheck"; import { Delegation } from "@/app/types/delegations"; import { ErrorHandlerParam, ErrorState } from "@/app/types/errors"; import { FinalityProvider as FinalityProviderInterface } from "@/app/types/finalityProviders"; @@ -39,6 +40,8 @@ import { StakingFee } from "./Form/StakingFee"; import { StakingTime } from "./Form/StakingTime"; import { Message } from "./Form/States/Message"; import { WalletNotConnected } from "./Form/States/WalletNotConnected"; +import apiNotAvailable from "./Form/States/api-not-available.svg"; +import geoRestricted from "./Form/States/geo-restricted.svg"; import stakingCapReached from "./Form/States/staking-cap-reached.svg"; import stakingNotStarted from "./Form/States/staking-not-started.svg"; import stakingUpgrading from "./Form/States/staking-upgrading.svg"; @@ -192,6 +195,7 @@ export const Staking: React.FC = ({ btcHeight + 1 < firstActivationHeight); const { isErrorOpen, showError } = useError(); + const { isApiNormal, isGeoBlocked, apiMessage } = useHealthCheck(); useEffect(() => { const handleError = ({ @@ -484,27 +488,39 @@ export const Staking: React.FC = ({ const renderStakingForm = () => { // States of the staking form: - // 1. Wallet is not connected - if (!isWalletConnected) { + // Health check failed + if (!isApiNormal || isGeoBlocked) { + return ( + + ); + } + // Wallet is not connected + else if (!isWalletConnected) { return ; } - // 2. Wallet is connected but we are still loading the staking params + // Wallet is connected but we are still loading the staking params else if (isLoading) { return ; } - // 3. Staking has not started yet + // Staking has not started yet else if (isBlockHeightUnderActivation) { return ( ); } - // 4. Staking params upgrading + // Staking params upgrading else if (isUpgrading) { return ( = ({ /> ); } - // 5. Staking cap reached + // Staking cap reached else if (overflow.overTheCapRange) { return showOverflowWarning(overflow); } - // 6. Staking form + // Staking form else { const { minStakingAmountSat, diff --git a/src/app/hooks/useHealthCheck.ts b/src/app/hooks/useHealthCheck.ts new file mode 100644 index 00000000..63b45219 --- /dev/null +++ b/src/app/hooks/useHealthCheck.ts @@ -0,0 +1,48 @@ +import { useQuery } from "@tanstack/react-query"; +import { useEffect } from "react"; + +import { + getHealthCheck, + isGeoBlockedResult, +} from "@/app/services/healthCheckService"; +import { HealthCheckStatus } from "@/app/types/services/healthCheck"; + +import { useError } from "../context/Error/ErrorContext"; +import { ErrorState } from "../types/errors"; + +export const useHealthCheck = () => { + const { showError } = useError(); + + const { data, error, isError, refetch } = useQuery({ + queryKey: ["api available"], + queryFn: getHealthCheck, + refetchOnMount: false, + refetchOnWindowFocus: false, + }); + + useEffect(() => { + if (isError) { + showError({ + error: { + message: error.message, + errorState: ErrorState.SERVER_ERROR, + errorTime: new Date(), + }, + retryAction: refetch, + }); + } + }, [isError, error, showError, refetch]); + + const isApiNormal = data?.status === HealthCheckStatus.Normal; + const isGeoBlocked = data ? isGeoBlockedResult(data) : false; + const apiMessage = data?.message; + + return { + isApiNormal, + isGeoBlocked, + apiMessage, + isError, + error, + refetch, + }; +}; diff --git a/src/app/services/healthCheckService.ts b/src/app/services/healthCheckService.ts new file mode 100644 index 00000000..9bdadd7a --- /dev/null +++ b/src/app/services/healthCheckService.ts @@ -0,0 +1,38 @@ +import { isAxiosError451 } from "../api/error"; +import { fetchHealthCheck } from "../api/healthCheckClient"; +import { + API_ERROR_MESSAGE, + GEO_BLOCK_MESSAGE, + HealthCheckResult, + HealthCheckStatus, +} from "../types/services/healthCheck"; + +export const getHealthCheck = async (): Promise => { + try { + const healthCheckAPIResponse = await fetchHealthCheck(); + if (healthCheckAPIResponse.data) { + return { + status: HealthCheckStatus.Normal, + message: healthCheckAPIResponse.data, + }; + } else { + throw new Error(API_ERROR_MESSAGE); + } + } catch (error: any) { + if (isAxiosError451(error)) { + return { + status: HealthCheckStatus.GeoBlocked, + message: GEO_BLOCK_MESSAGE, + }; + } else { + return { + status: HealthCheckStatus.Error, + message: error.response?.message || error?.message || API_ERROR_MESSAGE, + }; + } + } +}; + +export const isGeoBlockedResult = (result: HealthCheckResult): boolean => { + return result.status === HealthCheckStatus.GeoBlocked; +}; diff --git a/src/app/types/api/healthCheck.ts b/src/app/types/api/healthCheck.ts new file mode 100644 index 00000000..76d10177 --- /dev/null +++ b/src/app/types/api/healthCheck.ts @@ -0,0 +1,3 @@ +export interface HealthCheckResponse { + data: string; +} diff --git a/src/app/types/services/healthCheck.ts b/src/app/types/services/healthCheck.ts new file mode 100644 index 00000000..5ee85052 --- /dev/null +++ b/src/app/types/services/healthCheck.ts @@ -0,0 +1,15 @@ +export type HealthCheckResult = + | { status: HealthCheckStatus.Normal; message: string } + | { status: HealthCheckStatus.GeoBlocked; message: string } + | { status: HealthCheckStatus.Error; message: string }; + +export enum HealthCheckStatus { + Normal = "normal", + GeoBlocked = "geoblocked", + Error = "error", +} + +export const API_ERROR_MESSAGE = + "Error occurred while fetching API. Please try again later"; +export const GEO_BLOCK_MESSAGE = + "The Bitcoin Staking functionality is not available in your region"; diff --git a/src/utils/mempool_api.ts b/src/utils/mempool_api.ts index 2d444b6e..d5988a89 100644 --- a/src/utils/mempool_api.ts +++ b/src/utils/mempool_api.ts @@ -82,7 +82,6 @@ export async function pushTx(txHex: string): Promise { * @returns A promise that resolves to the amount of satoshis that the address * holds. */ -// TODO balance should come from reduced UTXOs export async function getAddressBalance(address: string): Promise { const response = await fetch(addressInfoUrl(address)); if (!response.ok) { diff --git a/src/utils/utxo/index.ts b/src/utils/utxo/index.ts index c27f697c..fb72d5a5 100644 --- a/src/utils/utxo/index.ts +++ b/src/utils/utxo/index.ts @@ -1,6 +1,6 @@ import { postVerifyUtxoOrdinals, UtxoInfo } from "@/app/api/postFilterOrdinals"; -import { Inscription, UTXO } from "../wallet/wallet_provider"; +import { InscriptionIdentifier, UTXO } from "../wallet/wallet_provider"; /** * Filters out UTXOs that contain ordinals. @@ -17,11 +17,15 @@ import { Inscription, UTXO } from "../wallet/wallet_provider"; export const filterOrdinals = async ( utxos: UTXO[], address: string, - getInscriptionsFromWalletCb: () => Promise, + getInscriptionsFromWalletCb?: () => Promise, ): Promise => { if (!utxos.length) { return []; } + // fallback to Babylon API if the wallet does not support getting inscriptions + if (!getInscriptionsFromWalletCb) { + return filterFromApi(utxos, address); + } // try to get the ordinals from the wallet first, if the wallet supports it // otherwise fallback to the Babylon API try { @@ -29,7 +33,9 @@ export const filterOrdinals = async ( // filter out the utxos that contains ordinals return utxos.filter( (utxo) => - !inscriptions.find((i) => i.output === `${utxo.txid}:${utxo.vout}`), + !inscriptions.find((i) => { + return i.txid === utxo.txid && i.vout === utxo.vout; + }), ); } catch (error) { return filterFromApi(utxos, address); diff --git a/src/utils/wallet/icons/bitget-wallet.svg b/src/utils/wallet/icons/bitget-wallet.svg deleted file mode 100644 index 65931c4c..00000000 --- a/src/utils/wallet/icons/bitget-wallet.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/utils/wallet/icons/bitget.svg b/src/utils/wallet/icons/bitget.svg new file mode 100644 index 00000000..f8794c91 --- /dev/null +++ b/src/utils/wallet/icons/bitget.svg @@ -0,0 +1,33 @@ + + + Group + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/utils/wallet/icons/tomo.svg b/src/utils/wallet/icons/tomo.svg index af092649..d668ecd5 100644 --- a/src/utils/wallet/icons/tomo.svg +++ b/src/utils/wallet/icons/tomo.svg @@ -1,13 +1,23 @@ - - - - - - - - - - - - - + + + tomo + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/utils/wallet/list.ts b/src/utils/wallet/list.ts index 9411a9fb..e813810f 100644 --- a/src/utils/wallet/list.ts +++ b/src/utils/wallet/list.ts @@ -1,4 +1,4 @@ -import bitgetWalletIcon from "./icons/bitget-wallet.svg"; +import bitgetWalletIcon from "./icons/bitget.svg"; import keystoneIcon from "./icons/keystone.svg"; import okxIcon from "./icons/okx.svg"; import oneKeyIcon from "./icons/onekey.svg"; diff --git a/src/utils/wallet/providers/bitget_wallet.ts b/src/utils/wallet/providers/bitget_wallet.ts index 52edb8c0..28980def 100644 --- a/src/utils/wallet/providers/bitget_wallet.ts +++ b/src/utils/wallet/providers/bitget_wallet.ts @@ -11,7 +11,7 @@ import { } from "../../mempool_api"; import { Fees, - Inscription, + InscriptionIdentifier, Network, UTXO, WalletProvider, @@ -210,7 +210,7 @@ export class BitgetWallet extends WalletProvider { return await getTipHeight(); }; - getInscriptions(): Promise { + getInscriptions(): Promise { throw new Error("Method not implemented."); } } diff --git a/src/utils/wallet/providers/keystone/index.ts b/src/utils/wallet/providers/keystone/index.ts index 12c9f5ed..6151e07d 100644 --- a/src/utils/wallet/providers/keystone/index.ts +++ b/src/utils/wallet/providers/keystone/index.ts @@ -32,7 +32,7 @@ import { import { WalletError, WalletErrorType } from "../../errors"; import { Fees, - Inscription, + InscriptionIdentifier, Network, UTXO, WalletProvider, @@ -292,7 +292,7 @@ export class KeystoneWallet extends WalletProvider { return await getTipHeight(); }; - getInscriptions(): Promise { + getInscriptions(): Promise { throw new Error("Method not implemented."); } } diff --git a/src/utils/wallet/providers/okx_wallet.ts b/src/utils/wallet/providers/okx_wallet.ts index dc233885..d0610270 100644 --- a/src/utils/wallet/providers/okx_wallet.ts +++ b/src/utils/wallet/providers/okx_wallet.ts @@ -12,9 +12,8 @@ import { pushTx, } from "../../mempool_api"; import { - DEFAULT_INSCRIPTION_LIMIT, Fees, - Inscription, + InscriptionIdentifier, Network, UTXO, WalletInfo, @@ -176,23 +175,51 @@ export class OKXWallet extends WalletProvider { }; // Inscriptions are only available on OKX Wallet BTC mainnet (i.e okxWallet.bitcoin) - getInscriptions = async (): Promise => { + getInscriptions = async (): Promise => { if (!this.okxWalletInfo) { throw new Error("OKX Wallet not connected"); } - const inscriptions: Inscription[] = []; - let cursor = 0; - while (true) { - const { list } = await this.bitcoinNetworkProvider.getInscriptions( - cursor, - DEFAULT_INSCRIPTION_LIMIT, + if (this.networkEnv !== Network.MAINNET) { + throw new Error( + "Inscriptions are only available on OKX Wallet BTC mainnet", ); - inscriptions.push(...list); - if (list.length < DEFAULT_INSCRIPTION_LIMIT) { - break; + } + // max num of iterations to prevent infinite loop + const MAX_ITERATIONS = 100; + // Fetch inscriptions in batches of 100 + const limit = 100; + const inscriptionIdentifiers: InscriptionIdentifier[] = []; + let cursor = 0; + let iterations = 0; + try { + while (iterations < MAX_ITERATIONS) { + const { list } = await this.bitcoinNetworkProvider.getInscriptions( + cursor, + limit, + ); + const identifiers = list.map((i: { output: string }) => { + const [txid, vout] = i.output.split(":"); + return { + txid, + vout, + }; + }); + inscriptionIdentifiers.push(...identifiers); + if (list.length < limit) { + break; + } + cursor += limit; + iterations++; + if (iterations >= MAX_ITERATIONS) { + throw new Error( + "Exceeded maximum iterations when fetching inscriptions", + ); + } } - cursor += DEFAULT_INSCRIPTION_LIMIT; + } catch (error) { + throw new Error("Failed to get inscriptions from OKX Wallet"); } - return inscriptions; + + return inscriptionIdentifiers; }; } diff --git a/src/utils/wallet/providers/onekey_wallet.ts b/src/utils/wallet/providers/onekey_wallet.ts index e8048c34..19bc6a9c 100644 --- a/src/utils/wallet/providers/onekey_wallet.ts +++ b/src/utils/wallet/providers/onekey_wallet.ts @@ -7,9 +7,8 @@ import { pushTx, } from "../../mempool_api"; import { - DEFAULT_INSCRIPTION_LIMIT, Fees, - Inscription, + InscriptionIdentifier, Network, UTXO, WalletInfo, @@ -165,20 +164,46 @@ export class OneKeyWallet extends WalletProvider { } // Inscriptions are only available on oneKey Wallet BTC mainnet - getInscriptions = async (): Promise => { - const inscriptions: Inscription[] = []; + getInscriptions = async (): Promise => { + if (this.networkEnv !== Network.MAINNET) { + throw new Error("Inscriptions are only available on OneKey mainnet"); + } + // max num of iterations to prevent infinite loop + const MAX_ITERATIONS = 100; + // Fetch inscriptions in batches of 100 + const limit = 100; + const inscriptionIdentifiers: InscriptionIdentifier[] = []; let cursor = 0; - while (true) { - const { list } = await this.bitcoinNetworkProvider.getInscriptions( - cursor, - DEFAULT_INSCRIPTION_LIMIT, - ); - inscriptions.push(...list); - if (list.length < DEFAULT_INSCRIPTION_LIMIT) { - break; + let iterations = 0; + try { + while (iterations < MAX_ITERATIONS) { + const { list } = await this.bitcoinNetworkProvider.getInscriptions( + cursor, + limit, + ); + const identifiers = list.map((i: { output: string }) => { + const [txid, vout] = i.output.split(":"); + return { + txid, + vout, + }; + }); + inscriptionIdentifiers.push(...identifiers); + if (list.length < limit) { + break; + } + cursor += limit; + iterations++; + if (iterations >= MAX_ITERATIONS) { + throw new Error( + "Exceeded maximum iterations when fetching inscriptions", + ); + } } - cursor += DEFAULT_INSCRIPTION_LIMIT; + } catch (error) { + throw new Error("Failed to get inscriptions from OKX Wallet"); } - return inscriptions; + + return inscriptionIdentifiers; }; } diff --git a/src/utils/wallet/providers/tomo_wallet.ts b/src/utils/wallet/providers/tomo_wallet.ts index 699ff06f..ed25c0ae 100644 --- a/src/utils/wallet/providers/tomo_wallet.ts +++ b/src/utils/wallet/providers/tomo_wallet.ts @@ -9,7 +9,7 @@ import { import { Fees, - Inscription, + InscriptionIdentifier, Network, UTXO, WalletInfo, @@ -171,7 +171,7 @@ export class TomoWallet extends WalletProvider { return await getTipHeight(); }; - getInscriptions(): Promise { + getInscriptions(): Promise { throw new Error("Method not implemented."); } } diff --git a/src/utils/wallet/wallet_provider.ts b/src/utils/wallet/wallet_provider.ts index ad4699a5..89992912 100644 --- a/src/utils/wallet/wallet_provider.ts +++ b/src/utils/wallet/wallet_provider.ts @@ -23,11 +23,12 @@ export interface UTXO { scriptPubKey: string; } -export interface Inscription { - // output of the inscription in the format of `txid:vout` - output: string; +export interface InscriptionIdentifier { + // hash of transaction that holds the ordinals/brc-2-/runes etc in the UTXO + txid: string; + // index of the output in the transaction + vout: number; } - // supported networks export enum Network { MAINNET = "mainnet", @@ -41,9 +42,6 @@ export type WalletInfo = { address: string; }; -// Default number of inscriptions to fetch in a single method call -export const DEFAULT_INSCRIPTION_LIMIT = 100; - /** * Abstract class representing a wallet provider. * Provides methods for connecting to a wallet, retrieving wallet information, signing transactions, and more. @@ -153,5 +151,5 @@ export abstract class WalletProvider { * Retrieves the inscriptions for the connected wallet. * @returns A promise that resolves to an array of inscriptions. */ - abstract getInscriptions(): Promise; + abstract getInscriptions(): Promise; } diff --git a/tests/utils/utox/filterOrdinals.test.ts b/tests/utils/utox/utxo.test.ts similarity index 92% rename from tests/utils/utox/filterOrdinals.test.ts rename to tests/utils/utox/utxo.test.ts index c306bc34..829c534a 100644 --- a/tests/utils/utox/filterOrdinals.test.ts +++ b/tests/utils/utox/utxo.test.ts @@ -1,6 +1,6 @@ import { postVerifyUtxoOrdinals } from "@/app/api/postFilterOrdinals"; import { filterOrdinals } from "@/utils/utxo"; -import { Inscription, UTXO } from "@/utils/wallet/wallet_provider"; +import { InscriptionIdentifier, UTXO } from "@/utils/wallet/wallet_provider"; // Mock the dependencies jest.mock("@/app/api/postFilterOrdinals"); @@ -23,7 +23,12 @@ describe("filterOrdinals", () => { }); it("should filter out UTXOs that contain ordinals from the wallet", async () => { - const mockInscriptions: Inscription[] = [{ output: "txid1:0" }]; + const mockInscriptions: InscriptionIdentifier[] = [ + { + txid: "txid1", + vout: 0, + }, + ]; const getInscriptionsFromWalletCb = jest .fn() .mockResolvedValue(mockInscriptions);