From 93c67e5cf55af98b83bcedb01840271ce9958383 Mon Sep 17 00:00:00 2001 From: Gonzalo D'elia Date: Sat, 11 Jan 2025 18:09:06 -0300 Subject: [PATCH 1/2] Sync history with past btc withdrawals --- webapp/utils/sync-history/bitcoin.ts | 237 ++++++++++++++++++++++++--- webapp/workers/history.ts | 7 +- 2 files changed, 221 insertions(+), 23 deletions(-) diff --git a/webapp/utils/sync-history/bitcoin.ts b/webapp/utils/sync-history/bitcoin.ts index 1100b0ac..c22e8fe5 100644 --- a/webapp/utils/sync-history/bitcoin.ts +++ b/webapp/utils/sync-history/bitcoin.ts @@ -1,33 +1,59 @@ import { MessageDirection } from '@eth-optimism/sdk' import { BtcChain } from 'btc-wallet/chains' import { Account, BtcTransaction } from 'btc-wallet/unisat' +import { bitcoinTunnelManagerAbi } from 'hemi-viem/contracts' +import { HemiPublicClient, publicClientToHemiClient } from 'hooks/useHemiClient' import { - type HemiPublicClient, - publicClientToHemiClient, -} from 'hooks/useHemiClient' -import { TransactionListSyncType } from 'hooks/useSyncHistory/types' + type BlockSyncType, + type TransactionListSyncType, +} from 'hooks/useSyncHistory/types' import pAll from 'p-all' -import { BtcDepositOperation, BtcDepositStatus } from 'types/tunnel' +import pThrottle from 'p-throttle' +import { + createSlidingBlockWindow, + CreateSlidingBlockWindow, +} from 'sliding-block-window/src' +import { + type BtcDepositOperation, + BtcDepositStatus, + BtcWithdrawStatus, + type ToBtcWithdrawOperation, +} from 'types/tunnel' import { calculateDepositAmount, getBitcoinTimestamp } from 'utils/bitcoin' import { createBtcApi, mapBitcoinNetwork, type MempoolJsBitcoinTransaction, } from 'utils/btcApi' +import { getEvmBlock } from 'utils/evmApi' import { getHemiStatusOfBtcDeposit, + getHemiStatusOfBtcWithdrawal, hemiAddressToBitcoinOpReturn, } from 'utils/hemi' import { getBitcoinCustodyAddress, getVaultAddressByIndex, } from 'utils/hemiMemoized' +import { createPublicProvider } from 'utils/providers' import { getNativeToken } from 'utils/token' -import { type Address, createPublicClient, http, toHex } from 'viem' +import { + type Address, + createPublicClient, + decodeFunctionData, + Hash, + http, + parseAbiItem, + toHex, + zeroAddress, +} from 'viem' +import { getBlockNumber, getBlockPayload } from './common' import { createSlidingTransactionList } from './slidingTransactionList' import { type HistorySyncer } from './types' +const throttlingOptions = { interval: 2000, limit: 1, strict: true } + const discardKnownTransactions = (toKnownTx?: BtcTransaction) => function (transactions: MempoolJsBitcoinTransaction[]) { if (!toKnownTx) { @@ -40,6 +66,24 @@ const discardKnownTransactions = (toKnownTx?: BtcTransaction) => return transactions.filter((_, i) => i < toIndex) } +const getWithdrawerBitcoinAddress = ({ + hash, + hemiClient, +}: { + hash: Hash + hemiClient: HemiPublicClient +}) => + hemiClient + .getTransaction({ hash }) + .then(({ input }) => + decodeFunctionData({ + abi: bitcoinTunnelManagerAbi, + data: input, + }), + ) + // the bitcoin address can be retrieve from the input data call - it's the 2nd parameter + .then(args => args[1] as string) + const isValidDeposit = ( hemiAddress: Address, opReturnUtxo: MempoolJsBitcoinTransaction['vout'][number], @@ -81,10 +125,99 @@ export const createBitcoinSync = function ({ l1Chain, l2Chain, saveHistory, -}: Omit, 'l1Chain'> & { + withdrawalsSyncInfo, +}: Omit< + HistorySyncer, + 'l1Chain' | 'withdrawalsSyncInfo' +> & { l1Chain: BtcChain -}) { - const syncDeposits = async function (hemiClient: HemiPublicClient) { +} & Pick, 'withdrawalsSyncInfo'>) { + const l2PublicClient = createPublicClient({ + chain: l2Chain, + transport: http(), + }) + + const hemiClient = publicClientToHemiClient(l2PublicClient) + + const getBitcoinWithdrawals = ({ + fromBlock, + toBlock, + }: { + fromBlock: number + toBlock: number + }) => + hemiClient + .getLogs({ + args: { + withdrawer: hemiAddress, + }, + event: parseAbiItem( + 'event WithdrawalInitiated(address indexed vault, address indexed withdrawer, string indexed btcAddress, uint256 withdrawalSats, uint256 netSatsAfterFee, uint64 uuid)', + ), + fromBlock: BigInt(fromBlock), + toBlock: BigInt(toBlock), + }) + .then(logs => + logs.map( + ({ args, blockNumber, transactionHash }) => + ({ + amount: args.withdrawalSats.toString(), + blockNumber: Number(blockNumber), + direction: MessageDirection.L2_TO_L1, + from: hemiAddress, + l1ChainId: l1Chain.id, + l1Token: zeroAddress, + l2ChainId: l2Chain.id, + l2Token: getNativeToken(l1Chain.id).extensions.bridgeInfo[ + l2Chain.id + ].tokenAddress, + // as logs are found, the tx is confirmed. So TX_CONFIRMED is the min status. + status: BtcWithdrawStatus.INITIATE_WITHDRAW_CONFIRMED, + transactionHash, + uuid: args.uuid.toString(), + }) satisfies Omit, + ), + ) + .then(withdrawals => + pAll( + withdrawals.map( + // pAll only infers the return type correctly if the function is async + w => async () => + Promise.all([ + getWithdrawerBitcoinAddress({ + hash: w.transactionHash, + hemiClient, + }), + getEvmBlock(w.blockNumber, w.l2ChainId), + ]) + .then( + ([btcAddress, block]) => + ({ + ...w, + timestamp: Number(block.timestamp), + to: btcAddress, + }) satisfies ToBtcWithdrawOperation, + ) + .then(withdrawal => + // status requires the timestamp to be defined, so this step must be done at last + getHemiStatusOfBtcWithdrawal({ + hemiClient, + // only value missing is "to", which is not used internally. + withdrawal: withdrawal as ToBtcWithdrawOperation, + }).then( + status => + ({ + ...withdrawal, + status, + }) satisfies ToBtcWithdrawOperation, + ), + ), + ), + { concurrency: 2 }, + ), + ) + + const syncDeposits = async function () { let localDepositSyncInfo: TransactionListSyncType = { ...depositsSyncInfo, } @@ -201,20 +334,82 @@ export const createBitcoinSync = function ({ }).run() } - const syncHistory = function () { - const l2PublicClient = createPublicClient({ - chain: l2Chain, - transport: http(), - }) + const syncWithdrawals = async function () { + const lastBlock = await getBlockNumber( + withdrawalsSyncInfo.toBlock, + createPublicProvider(l2Chain.rpcUrls.default.http[0], l2Chain), + ) + + const initialBlock = + withdrawalsSyncInfo.fromBlock ?? withdrawalsSyncInfo.minBlockToSync ?? 0 + + debug( + 'Syncing withdrawals between blocks %s and %s', + initialBlock, + lastBlock, + ) + + const onChange = async function ({ + canMove, + nextState, + state, + }: Parameters[0]) { + // we walk the blockchain backwards, but OP API expects + // toBlock > fromBlock - so we must invert them + const { from: toBlock, to: fromBlock, windowIndex } = state + + debug( + 'Getting deposits from block %s to %s (windowIndex %s)', + fromBlock, + toBlock, + windowIndex, + ) + + const newWithdrawals = await getBitcoinWithdrawals({ + fromBlock, + toBlock, + }) + + debug( + 'Got %s withdrawals from block %s to %s (windowIndex %s). Saving', + newWithdrawals.length, + fromBlock, + toBlock, + windowIndex, + ) + + // save the withdrawals + saveHistory({ + payload: { + ...getBlockPayload({ + canMove, + fromBlock: withdrawalsSyncInfo.fromBlock, + lastBlock, + nextState, + }), + chainId: l1Chain.id, + content: newWithdrawals, + }, + type: 'sync-withdrawals', + }) + } + + return createSlidingBlockWindow({ + initialBlock, + lastBlock, + onChange: pThrottle(throttlingOptions)(onChange), + windowIndex: withdrawalsSyncInfo.chunkIndex, + windowSize: withdrawalsSyncInfo.blockWindowSize, + }).run() + } - const hemiClient = publicClientToHemiClient(l2PublicClient) + const syncHistory = async function () { + await Promise.all([ + syncDeposits().then(() => debug('Deposits sync finished')), + syncWithdrawals().then(() => debug('Withdrawals sync finished')), + ]) - return Promise.all([ - syncDeposits(hemiClient).then(() => debug('Deposits sync finished')), - // syncWithdrawals().then(() => debug('Withdrawals sync finished')), - ]).then(function () { - debug('Sync process finished') - }) + debug('Sync process finished') } return { syncHistory } diff --git a/webapp/workers/history.ts b/webapp/workers/history.ts index 80d5756b..1f40d126 100644 --- a/webapp/workers/history.ts +++ b/webapp/workers/history.ts @@ -64,8 +64,11 @@ const createSyncer = function ({ l1Chain, l2Chain, saveHistory, - withdrawalsSyncInfo: - withdrawalsSyncInfo as ExtendedSyncInfo, + withdrawalsSyncInfo: { + ...(withdrawalsSyncInfo as ExtendedSyncInfo), + // depending on L1 (bitcoin) chain, get config for L2 (Hemi) + ...chainConfiguration[l1Chain.id], + }, }) case mainnet.id: case sepolia.id: From e32a368a95acbd4d1b6fccf2e8fa4bf4e6eb930b Mon Sep 17 00:00:00 2001 From: Gonzalo D'elia Date: Thu, 16 Jan 2025 11:47:45 -0300 Subject: [PATCH 2/2] Refactor fn to get withdrawals --- webapp/utils/sync-history/bitcoin.ts | 176 ++++++++++++++++----------- 1 file changed, 105 insertions(+), 71 deletions(-) diff --git a/webapp/utils/sync-history/bitcoin.ts b/webapp/utils/sync-history/bitcoin.ts index c22e8fe5..f1242835 100644 --- a/webapp/utils/sync-history/bitcoin.ts +++ b/webapp/utils/sync-history/bitcoin.ts @@ -13,6 +13,7 @@ import { createSlidingBlockWindow, CreateSlidingBlockWindow, } from 'sliding-block-window/src' +import { type EvmChain } from 'types/chain' import { type BtcDepositOperation, BtcDepositStatus, @@ -41,8 +42,9 @@ import { type Address, createPublicClient, decodeFunctionData, - Hash, + type Hash, http, + type Log, parseAbiItem, toHex, zeroAddress, @@ -118,6 +120,100 @@ const filterDeposits = ( return isValidDeposit(hemiAddress, opReturnUtxo) }) +const addAdditionalInfo = + (hemiClient: HemiPublicClient) => + (withdrawals: Omit[]) => + pAll( + withdrawals.map( + // pAll only infers the return type correctly if the function is async + w => async () => + Promise.all([ + getWithdrawerBitcoinAddress({ + hash: w.transactionHash, + hemiClient, + }), + getEvmBlock(w.blockNumber, w.l2ChainId), + ]) + .then( + ([btcAddress, block]) => + ({ + ...w, + timestamp: Number(block.timestamp), + to: btcAddress, + }) satisfies ToBtcWithdrawOperation, + ) + .then(withdrawal => + // status requires the timestamp to be defined, so this step must be done at last + getHemiStatusOfBtcWithdrawal({ + hemiClient, + // only value missing is "to", which is not used internally. + withdrawal: withdrawal as ToBtcWithdrawOperation, + }).then( + status => + ({ + ...withdrawal, + status, + }) satisfies ToBtcWithdrawOperation, + ), + ), + ), + { concurrency: 2 }, + ) + +const withdrawalInitiatedAbiEvent = parseAbiItem( + 'event WithdrawalInitiated(address indexed vault, address indexed withdrawer, string indexed btcAddress, uint256 withdrawalSats, uint256 netSatsAfterFee, uint64 uuid)', +) + +const getWithdrawalsLogs = ({ + fromBlock, + hemiAddress, + hemiClient, + toBlock, +}: { + fromBlock: number + hemiAddress: Address + hemiClient: HemiPublicClient + toBlock: number +}) => + hemiClient.getLogs({ + args: { + withdrawer: hemiAddress, + }, + event: withdrawalInitiatedAbiEvent, + fromBlock: BigInt(fromBlock), + toBlock: BigInt(toBlock), + }) + +const logsToWithdrawals = + ({ + hemiAddress, + l1Chain, + l2Chain, + }: { + hemiAddress: Address + l1Chain: BtcChain + l2Chain: EvmChain + }) => + (logs: Log[]) => + logs.map( + ({ args, blockNumber, transactionHash }) => + ({ + amount: args.withdrawalSats.toString(), + blockNumber: Number(blockNumber), + direction: MessageDirection.L2_TO_L1, + from: hemiAddress, + l1ChainId: l1Chain.id, + l1Token: zeroAddress, + l2ChainId: l2Chain.id, + l2Token: getNativeToken(l1Chain.id).extensions.bridgeInfo[l2Chain.id] + .tokenAddress, + // as logs are found, the tx is confirmed. So TX_CONFIRMED is the min status. + status: BtcWithdrawStatus.INITIATE_WITHDRAW_CONFIRMED, + transactionHash, + uuid: args.uuid.toString(), + }) satisfies Omit, + ) + export const createBitcoinSync = function ({ address: hemiAddress, debug, @@ -146,76 +242,14 @@ export const createBitcoinSync = function ({ fromBlock: number toBlock: number }) => - hemiClient - .getLogs({ - args: { - withdrawer: hemiAddress, - }, - event: parseAbiItem( - 'event WithdrawalInitiated(address indexed vault, address indexed withdrawer, string indexed btcAddress, uint256 withdrawalSats, uint256 netSatsAfterFee, uint64 uuid)', - ), - fromBlock: BigInt(fromBlock), - toBlock: BigInt(toBlock), - }) - .then(logs => - logs.map( - ({ args, blockNumber, transactionHash }) => - ({ - amount: args.withdrawalSats.toString(), - blockNumber: Number(blockNumber), - direction: MessageDirection.L2_TO_L1, - from: hemiAddress, - l1ChainId: l1Chain.id, - l1Token: zeroAddress, - l2ChainId: l2Chain.id, - l2Token: getNativeToken(l1Chain.id).extensions.bridgeInfo[ - l2Chain.id - ].tokenAddress, - // as logs are found, the tx is confirmed. So TX_CONFIRMED is the min status. - status: BtcWithdrawStatus.INITIATE_WITHDRAW_CONFIRMED, - transactionHash, - uuid: args.uuid.toString(), - }) satisfies Omit, - ), - ) - .then(withdrawals => - pAll( - withdrawals.map( - // pAll only infers the return type correctly if the function is async - w => async () => - Promise.all([ - getWithdrawerBitcoinAddress({ - hash: w.transactionHash, - hemiClient, - }), - getEvmBlock(w.blockNumber, w.l2ChainId), - ]) - .then( - ([btcAddress, block]) => - ({ - ...w, - timestamp: Number(block.timestamp), - to: btcAddress, - }) satisfies ToBtcWithdrawOperation, - ) - .then(withdrawal => - // status requires the timestamp to be defined, so this step must be done at last - getHemiStatusOfBtcWithdrawal({ - hemiClient, - // only value missing is "to", which is not used internally. - withdrawal: withdrawal as ToBtcWithdrawOperation, - }).then( - status => - ({ - ...withdrawal, - status, - }) satisfies ToBtcWithdrawOperation, - ), - ), - ), - { concurrency: 2 }, - ), - ) + getWithdrawalsLogs({ + fromBlock, + hemiAddress, + hemiClient, + toBlock, + }) + .then(logsToWithdrawals({ hemiAddress, l1Chain, l2Chain })) + .then(addAdditionalInfo(hemiClient)) const syncDeposits = async function () { let localDepositSyncInfo: TransactionListSyncType = {