Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for mainnet in BTC Api #727

Merged
merged 4 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions webapp/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
NEXT_PUBLIC_BTC_INPUTS_SIZE=105
NEXT_PUBLIC_BTC_OUTPUTS_SIZE=25
# Once mainnet is published, these values below may go into .env.development as they belong to hemi's testnet
NEXT_PUBLIC_BITCOIN_NETWORK=testnet
NEXT_PUBLIC_MEMPOOL_API_URL="https://mempool.space/testnet/api"
14 changes: 9 additions & 5 deletions webapp/hooks/useEstimateBtcFees.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useQuery } from '@tanstack/react-query'
import { type Account } from 'btc-wallet/unisat'
import { getAddressUtxo, getRecommendedFees } from 'utils/btcApi'
import { createBtcApi } from 'utils/btcApi'

import { useNetworkType } from './useNetworkType'

const btcInputsSize = parseInt(process.env.NEXT_PUBLIC_BTC_INPUTS_SIZE)
const btcOutputsSize = parseInt(process.env.NEXT_PUBLIC_BTC_OUTPUTS_SIZE)
// the value sent + OP_RETURN with hemi address
const expectedOutputs = 2

export const useGetFeePrices = function () {
const [networkType] = useNetworkType()
const { data: feePrices, ...rest } = useQuery({
queryFn: getRecommendedFees,
queryKey: ['btc-recommended-fees'],
queryFn: () => createBtcApi(networkType).getRecommendedFees(),
queryKey: ['btc-recommended-fees', networkType],
// refetch every minute
refetchInterval: 1000 * 60,
})
Expand All @@ -21,10 +24,11 @@ export const useGetFeePrices = function () {
}

const useGetUtxos = function (account: Account) {
const [networkType] = useNetworkType()
const { data: utxos, ...rest } = useQuery({
enabled: !!account,
queryFn: () => getAddressUtxo(account),
queryKey: ['btc-utxos', account],
queryFn: () => createBtcApi(networkType).getAddressTxsUtxo(account),
queryKey: ['btc-utxos', account, networkType],
})
return {
utxos,
Expand Down
9 changes: 6 additions & 3 deletions webapp/hooks/useWaitForTransactionReceipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ import { useQuery } from '@tanstack/react-query'
import { useAccount } from 'btc-wallet/hooks/useAccount'
import { type BtcTransaction } from 'btc-wallet/unisat'
import { isChainIdSupported } from 'btc-wallet/utils/chains'
import { getTransactionReceipt } from 'utils/btcApi'
import { createBtcApi } from 'utils/btcApi'

import { useNetworkType } from './useNetworkType'

type Args = {
txId: BtcTransaction
}

export const useWaitForTransactionReceipt = function ({ txId }: Args) {
const { chainId } = useAccount()
const [networkType] = useNetworkType()

const queryKey = ['btc-wallet-wait-tx', chainId, txId]
const queryKey = ['btc-wallet-wait-tx', chainId, networkType, txId]

return {
...useQuery({
enabled: !!txId && !!chainId && isChainIdSupported(chainId),
queryFn: () => getTransactionReceipt(txId),
queryFn: () => createBtcApi(networkType).getTransactionReceipt(txId),
queryKey,
refetchInterval(query) {
// Poll every 30 secs until confirmed
Expand Down
2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"camelcase-keys": "9.1.3",
"crypto-shortener": "1.1.0",
"debug": "4.3.7",
"esplora-client": "1.1.0",
"esplora-client": "1.2.0",
"hemi-socials": "1.0.0",
"hemi-viem": "2.0.0-alpha.1",
"javascript-time-ago": "2.5.10",
Expand Down
19 changes: 13 additions & 6 deletions webapp/test/utils/watch/bitcoinDeposit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { bitcoinTestnet } from 'btc-wallet/chains'
import { hemiSepolia } from 'hemi-viem'
import { publicClientToHemiClient } from 'hooks/useHemiClient'
import { type BtcDepositOperation, BtcDepositStatus } from 'types/tunnel'
import { getTransactionReceipt } from 'utils/btcApi'
import { createBtcApi } from 'utils/btcApi'
import { getHemiStatusOfBtcDeposit, getVaultAddressByDeposit } from 'utils/hemi'
import {
watchDepositOnBitcoin,
Expand All @@ -27,9 +27,14 @@ vi.mock('hooks/useHemiClient', () => ({
publicClientToHemiClient: vi.fn(),
}))

vi.mock('utils/btcApi', () => ({
getTransactionReceipt: vi.fn(),
}))
// mock createBtcApi but keep the original mapBitcoinNetwork
vi.mock(import('utils/btcApi'), async function (importOriginal) {
const btcApi = await importOriginal()
return {
...btcApi,
createBtcApi: vi.fn(),
}
})

vi.mock('utils/hemi', () => ({
getHemiStatusOfBtcDeposit: vi.fn(),
Expand All @@ -43,9 +48,10 @@ describe('utils/watch/bitcoinDeposits', function () {

describe('watchDepositOnBitcoin', function () {
it('should not return changes if the receipt show it is still not confirmed', async function () {
vi.mocked(getTransactionReceipt).mockResolvedValue({
const getTransactionReceipt = vi.fn().mockResolvedValue({
status: { confirmed: false },
})
vi.mocked(createBtcApi).mockReturnValue({ getTransactionReceipt })

const updates = await watchDepositOnBitcoin(deposit)

Expand All @@ -59,9 +65,10 @@ describe('utils/watch/bitcoinDeposits', function () {
it('should return the new status, timestamp and block height if the receipt shows confirmation', async function () {
const blockHeight = 123
const blockTime = 456
vi.mocked(getTransactionReceipt).mockResolvedValue({
const getTransactionReceipt = vi.fn().mockResolvedValue({
status: { blockHeight, blockTime, confirmed: true },
})
vi.mocked(createBtcApi).mockReturnValue({ getTransactionReceipt })

const updates = await watchDepositOnBitcoin(deposit)

Expand Down
124 changes: 71 additions & 53 deletions webapp/utils/btcApi.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Account, BtcTransaction, Satoshis } from 'btc-wallet/unisat'
import {
Account,
BtcSupportedNetworks,
BtcTransaction,
Satoshis,
} from 'btc-wallet/unisat'
import camelCaseKeys from 'camelcase-keys'
import { esploraClient } from 'esplora-client'
import { NetworkType } from 'hooks/useNetworkType'

const toCamelCase = <T extends Record<string, unknown>>(obj: T) =>
camelCaseKeys(obj, { deep: true })

const network = process.env.NEXT_PUBLIC_BITCOIN_NETWORK
const { bitcoin } = esploraClient({ network })

type TransactionStatus = {
blockTime?: number
confirmed: boolean
Expand Down Expand Up @@ -37,24 +40,15 @@ type Utxo = {
value: Satoshis
}

// See https://mempool.space/docs/api/rest#get-address-transactions
export const getAddressTransactions = (
address: Account,
queryString?: { afterTxId: string },
) =>
bitcoin.addresses
.getAddressTxs({ address, after_txid: queryString?.afterTxId })
.then(txs =>
txs.map(tx => toCamelCase({ ...tx, txId: tx.txid })),
) as Promise<MempoolJsBitcoinTransaction[]>

// See https://mempool.space/docs/api/rest#get-address-utxo (we are converting to camelCase)
export const getAddressUtxo = (address: Account) =>
bitcoin.addresses
.getAddressUtxo({ address })
.then(utxos =>
utxos.map(utxo => toCamelCase({ ...utxo, txId: utxo.txid })),
) as Promise<Utxo[]>
export type TransactionReceipt = {
txId: BtcTransaction
status: {
blockHeight?: number
blockTime?: number
confirmed: boolean
}
vout: Vout[]
}

type Fees = {
fastestFee: number
Expand All @@ -63,37 +57,61 @@ type Fees = {
economyFee: number
minimumFee: number
}
// See https://mempool.space/docs/api/rest#get-recommended-fees
export const getRecommendedFees = () =>
bitcoin.fees.getFeesRecommended() as Promise<Fees>

export type TransactionReceipt = {
txId: BtcTransaction
status: {
blockHeight?: number
blockTime?: number
confirmed: boolean
export const mapBitcoinNetwork = (network: BtcSupportedNetworks) =>
network === 'livenet' ? 'mainnet' : 'testnet'

export const createBtcApi = function (network: NetworkType) {
const { bitcoin } = esploraClient({ network })

// See https://mempool.space/docs/api/rest#get-address-transactions
const getAddressTransactions = (
address: Account,
queryString?: { afterTxId: string },
) =>
bitcoin.addresses
.getAddressTxs({ address, after_txid: queryString?.afterTxId })
.then(txs =>
txs.map(tx => toCamelCase({ ...tx, txId: tx.txid })),
) as Promise<MempoolJsBitcoinTransaction[]>

// See https://mempool.space/docs/api/rest#get-address-utxo (we are converting to camelCase)
const getAddressTxsUtxo = (address: Account) =>
bitcoin.addresses
.getAddressTxsUtxo({ address })
.then(utxos =>
utxos.map(utxo => toCamelCase({ ...utxo, txId: utxo.txid })),
) as Promise<Utxo[]>

// See https://mempool.space/docs/api/rest#get-recommended-fees
const getRecommendedFees = () =>
bitcoin.fees.getFeesRecommended() as Promise<Fees>

// See https://mempool.space/testnet/docs/api/rest#get-transaction (we are converting the keys to camelCase)
const getTransactionReceipt = (txId: BtcTransaction) =>
bitcoin.transactions
.getTx({ txid: txId })
.catch(function (err) {
if (err?.message.includes('not found')) {
// It seems it takes a couple of seconds for the Tx for being picked up
// react-query doesn't let us to return undefined data, so we must
// return an unconfirmed status
// Once it appears in the mempool, it will return the full object
// with the same confirmation status as false.
return { status: { confirmed: false } }
}
throw err
})
.then(toCamelCase)
.then(({ txid, ...rest }) => ({
txId: txid,
...rest,
})) as Promise<TransactionReceipt | undefined>

return {
getAddressTransactions,
getAddressTxsUtxo,
getRecommendedFees,
getTransactionReceipt,
}
vout: Vout[]
}

// See https://mempool.space/testnet/docs/api/rest#get-transaction (we are converting the keys to camelCase)
export const getTransactionReceipt = (txId: BtcTransaction) =>
bitcoin.transactions
.getTx({ txid: txId })
.catch(function (err) {
if (err?.message.includes('not found')) {
// It seems it takes a couple of seconds for the Tx for being picked up
// react-query doesn't let us to return undefined data, so we must
// return an unconfirmed status
// Once it appears in the mempool, it will return the full object
// with the same confirmation status as false.
return { status: { confirmed: false } }
}
throw err
})
.then(toCamelCase)
.then(({ txid, ...rest }) => ({
txId: txid,
...rest,
})) as Promise<TransactionReceipt | undefined>
8 changes: 4 additions & 4 deletions webapp/utils/hemi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BtcDepositOperation, BtcDepositStatus } from 'types/tunnel'
import { type Address } from 'viem'

import { calculateDepositOutputIndex } from './bitcoin'
import { getTransactionReceipt } from './btcApi'
import { createBtcApi, mapBitcoinNetwork } from './btcApi'

// Max Sanchez note: looks like if we pass in all lower-case hex, Unisat publishes the bytes instead of the string.
// Tunnel for now is only validating the string representation, but update this in the future using
Expand Down Expand Up @@ -141,9 +141,9 @@ export const claimBtcDeposit = ({
getVaultAddressByDeposit(hemiClient, deposit).then(vaultAddress =>
getHemiStatusOfBtcDeposit({ deposit, hemiClient, vaultAddress }),
),
getTransactionReceipt(deposit.transactionHash).then(receipt =>
calculateDepositOutputIndex(receipt, deposit.to),
),
createBtcApi(mapBitcoinNetwork(deposit.l1ChainId))
.getTransactionReceipt(deposit.transactionHash)
.then(receipt => calculateDepositOutputIndex(receipt, deposit.to)),
]).then(function ([vaultIndex, currentStatus, outputIndex]) {
if (currentStatus === BtcDepositStatus.BTC_DEPOSITED) {
throw new Error('Bitcoin Deposit already confirmed')
Expand Down
15 changes: 9 additions & 6 deletions webapp/utils/sync-history/bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import pAll from 'p-all'
import { BtcDepositOperation, BtcDepositStatus } from 'types/tunnel'
import { calculateDepositAmount, getBitcoinTimestamp } from 'utils/bitcoin'
import {
getAddressTransactions,
MempoolJsBitcoinTransaction,
createBtcApi,
mapBitcoinNetwork,
type MempoolJsBitcoinTransaction,
} from 'utils/btcApi'
import {
getBitcoinCustodyAddress,
Expand Down Expand Up @@ -101,10 +102,12 @@ export const createBitcoinSync = function ({
const formattedTxId = afterTxId ?? 'last available transaction'
debug('Getting transactions batch starting from %s', formattedTxId)

const transactions = await getAddressTransactions(
bitcoinCustodyAddress,
afterTxId ? { afterTxId } : undefined,
).then(discardKnownTransactions(localDepositSyncInfo.toKnownTx))
const transactions = await createBtcApi(mapBitcoinNetwork(l1Chain.id))
.getAddressTransactions(
bitcoinCustodyAddress,
afterTxId ? { afterTxId } : undefined,
)
.then(discardKnownTransactions(localDepositSyncInfo.toKnownTx))

debug(
'Found %s transactions starting from %s',
Expand Down
6 changes: 4 additions & 2 deletions webapp/utils/watch/bitcoinDeposits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { publicClientToHemiClient } from 'hooks/useHemiClient'
import pMemoize from 'promise-mem'
import { type BtcDepositOperation, BtcDepositStatus } from 'types/tunnel'
import { getBitcoinTimestamp } from 'utils/bitcoin'
import { getTransactionReceipt } from 'utils/btcApi'
import { createBtcApi, mapBitcoinNetwork } from 'utils/btcApi'
import { findChainById } from 'utils/chain'
import { getHemiStatusOfBtcDeposit, getVaultAddressByDeposit } from 'utils/hemi'
import { hasKeys } from 'utils/utilities'
Expand All @@ -15,7 +15,9 @@ export const watchDepositOnBitcoin = async function (
deposit: BtcDepositOperation,
) {
debug('Watching deposit %s', deposit.transactionHash)
const receipt = await getTransactionReceipt(deposit.transactionHash)
const receipt = await createBtcApi(
mapBitcoinNetwork(deposit.l1ChainId),
).getTransactionReceipt(deposit.transactionHash)
if (!receipt) {
debug('Receipt not found for deposit %s', deposit.transactionHash)
return {}
Expand Down
Loading