From fe0e242aab6756b0e9275cd9d76849378241e9d2 Mon Sep 17 00:00:00 2001 From: Crypto Minion <154598612+jrwbabylonlab@users.noreply.github.com> Date: Mon, 11 Nov 2024 20:49:44 +1100 Subject: [PATCH] Eoi service (#327) * feat: create EOI service --- .env.example | 2 +- package-lock.json | 16 ++ package.json | 1 + src/app/components/Modals/PreviewModal.tsx | 5 +- src/app/components/Staking/Staking.tsx | 239 +++-------------- .../context/wallet/CosmosWalletProvider.tsx | 38 ++- .../wallet/WalletConnectionProvider.tsx | 3 + src/app/hooks/api/useParams.ts | 4 +- .../hooks/services/useEoiCreationService.tsx | 244 ++++++++++++++++++ src/utils/delegations/index.ts | 46 ++++ src/utils/wallet/bbnRegistry.ts | 41 ++- 11 files changed, 407 insertions(+), 232 deletions(-) create mode 100644 src/app/hooks/services/useEoiCreationService.tsx create mode 100644 src/utils/delegations/index.ts diff --git a/.env.example b/.env.example index 9d129921..6b59863c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ NEXT_PUBLIC_MEMPOOL_API=https://mempool.space -NEXT_PUBLIC_API_URL=https://staking-api.testnet.babylonchain.io +NEXT_PUBLIC_API_URL=https://staking-api.phase-2-devnet.babylonlabs.io NEXT_PUBLIC_POINTS_API_URL=https://points.testnet.babylonchain.io NEXT_PUBLIC_NETWORK=signet NEXT_PUBLIC_DISPLAY_TESTING_MESSAGES=true diff --git a/package-lock.json b/package-lock.json index 84a5db27..970cc823 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "simple-staking", "version": "0.3.9", "dependencies": { + "@babylonlabs-io/babylon-proto-ts": "0.0.3-canary.2", "@babylonlabs-io/btc-staking-ts": "0.4.0-canary.2", "@bitcoin-js/tiny-secp256k1-asmjs": "2.2.3", "@bitcoinerlab/secp256k1": "^1.1.1", @@ -2056,6 +2057,21 @@ "node": ">=6.9.0" } }, + "node_modules/@babylonlabs-io/babylon-proto-ts": { + "version": "0.0.3-canary.2", + "resolved": "https://registry.npmjs.org/@babylonlabs-io/babylon-proto-ts/-/babylon-proto-ts-0.0.3-canary.2.tgz", + "integrity": "sha512-S6Xe+xMUakog2EGLyhKxE2ynSps0qggblxksArnw5GMZBEGqc0OBWBLtyU+QfcNM14KMLUjGnjzjulY2qFWTOA==", + "license": "ISC", + "dependencies": { + "@bufbuild/protobuf": "^2.2.0" + } + }, + "node_modules/@babylonlabs-io/babylon-proto-ts/node_modules/@bufbuild/protobuf": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz", + "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, "node_modules/@babylonlabs-io/btc-staking-ts": { "version": "0.4.0-canary.2", "resolved": "https://registry.npmjs.org/@babylonlabs-io/btc-staking-ts/-/btc-staking-ts-0.4.0-canary.2.tgz", diff --git a/package.json b/package.json index fbf4c374..5c25245e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "node": "22.3.0" }, "dependencies": { + "@babylonlabs-io/babylon-proto-ts": "0.0.3-canary.2", "@babylonlabs-io/btc-staking-ts": "0.4.0-canary.2", "@bitcoin-js/tiny-secp256k1-asmjs": "2.2.3", "@bitcoinerlab/secp256k1": "^1.1.1", diff --git a/src/app/components/Modals/PreviewModal.tsx b/src/app/components/Modals/PreviewModal.tsx index dc89dd96..6495cab3 100644 --- a/src/app/components/Modals/PreviewModal.tsx +++ b/src/app/components/Modals/PreviewModal.tsx @@ -19,7 +19,6 @@ interface PreviewModalProps { stakingTimeBlocks: number; stakingFeeSat: number; feeRate: number; - confirmationDepth: number; unbondingFeeSat: number; awaitingWalletResponse: boolean; } @@ -33,7 +32,6 @@ export const PreviewModal: React.FC = ({ onSign, stakingFeeSat, feeRate, - confirmationDepth, unbondingFeeSat, awaitingWalletResponse, }) => { @@ -42,6 +40,9 @@ export const PreviewModal: React.FC = ({ const { coinName } = getNetworkConfig(); + // TODO: Get confirmation depth from params + const confirmationDepth = 10; + return ( { const { availableUTXOs, - currentHeight: btcHeight, currentVersion, totalBalance: btcWalletBalanceSat, - firstActivationHeight, - isApprochingNextVersion, isError, isLoading, } = useAppState(); - const { addDelegation } = useDelegationState(); const { connected, address, publicKeyNoCoord, network: btcWalletNetwork, getNetworkFees, - signPsbt, - pushTx, } = useBTCWallet(); const disabled = isError; @@ -96,11 +74,9 @@ export const Staking = () => { useLocalStorage("bbn-staking-successFeedbackModalOpened", false); const [cancelFeedbackModalOpened, setCancelFeedbackModalOpened] = useLocalStorage("bbn-staking-cancelFeedbackModalOpened ", false); - const [overflow, setOverflow] = useState({ - isHeightCap: false, - overTheCapRange: false, - approchingCapRange: false, - }); + + const { createDelegationEoi } = useEoiCreationService(); + const { data: params } = useParams(); // Mempool fee rates, comes from the network // Fetch fee rates, sat/vB @@ -120,50 +96,6 @@ export const Staking = () => { }, }); - const stakingStats = useStakingStats(); - - // Calculate the overflow properties - useEffect(() => { - if (!currentVersion || !btcHeight) { - return; - } - const nextBlockHeight = btcHeight + 1; - const { stakingCapHeight, stakingCapSat } = currentVersion; - // Use height based cap than value based cap if it is set - if (stakingCapHeight) { - setOverflow({ - isHeightCap: true, - overTheCapRange: nextBlockHeight > stakingCapHeight, - /* - When btc height is approching the staking cap height, - there is higher chance of overflow due to tx not being included in the next few blocks on time - We also don't take the confirmation depth into account here as majority - of the delegation will be overflow after the cap is reached, unless btc fork happens but it's unlikely - */ - approchingCapRange: - nextBlockHeight >= - stakingCapHeight - OVERFLOW_HEIGHT_WARNING_THRESHOLD, - }); - } else if (stakingCapSat && stakingStats.data) { - const { activeTVLSat, unconfirmedTVLSat } = stakingStats.data; - setOverflow({ - isHeightCap: false, - overTheCapRange: stakingCapSat <= activeTVLSat, - approchingCapRange: - stakingCapSat * OVERFLOW_TVL_WARNING_THRESHOLD < unconfirmedTVLSat, - }); - } - }, [currentVersion, btcHeight, stakingStats]); - - const { coinName } = getNetworkConfig(); - const stakingParams = currentVersion; - const isUpgrading = isApprochingNextVersion; - const isBlockHeightUnderActivation = - !stakingParams || - (btcHeight && - firstActivationHeight && - btcHeight + 1 < firstActivationHeight); - const { isErrorOpen, showError, captureError } = useError(); const { isApiNormal, isGeoBlocked, apiMessage } = useHealthCheck(); @@ -217,40 +149,24 @@ export const Staking = () => { const queryClient = useQueryClient(); - const handleSign = async () => { + // TODO: To hook up with the react signing modal + const signingCallback = async (step: SigningStep) => { + console.log("Signing step:", step); + }; + + const handleDelegationEoiCreation = async () => { try { - // Prevent the modal from closing - setAwaitingWalletResponse(true); - // Initial validation - if (!connected) throw new Error("Wallet is not connected"); - if (!address) throw new Error("Address is not set"); - if (!btcWalletNetwork) throw new Error("Wallet network is not connected"); - if (!finalityProvider) - throw new Error("Finality provider is not selected"); - if (!currentVersion) throw new Error("Global params not loaded"); - if (!feeRate) throw new Error("Fee rates not loaded"); - if (!availableUTXOs || availableUTXOs.length === 0) - throw new Error("No available balance"); - - // Sign the staking transaction - const { stakingTxHex, stakingTerm } = await signStakingTx( - signPsbt, - pushTx, - currentVersion, + if (!finalityProvider) { + throw new Error("Finality provider not selected"); + } + const eoiInput = { + finalityProviderPublicKey: finalityProvider.btcPk, stakingAmountSat, stakingTimeBlocks, - finalityProvider.btcPk, - btcWalletNetwork, - address, - publicKeyNoCoord, feeRate, - availableUTXOs, - ); - // Invalidate UTXOs - queryClient.invalidateQueries({ queryKey: [UTXO_KEY, address] }); - // UI - handleFeedbackModal("success"); - handleLocalStorageDelegations(stakingTxHex, stakingTerm); + }; + await createDelegationEoi(eoiInput, signingCallback); + // TODO: Hook up with the react pending verify modal handleResetState(); } catch (error: Error | any) { showError({ @@ -274,26 +190,6 @@ export const Staking = () => { } }; - // Save the delegation to local storage - const handleLocalStorageDelegations = ( - signedTxHex: string, - stakingTerm: number, - ) => { - // Get the transaction ID - const newTxId = Transaction.fromHex(signedTxHex).getId(); - - addDelegation( - toLocalStorageDelegation( - newTxId, - publicKeyNoCoord, - finalityProvider!.btcPk, - stakingAmountSat, - signedTxHex, - stakingTerm, - ), - ); - }; - // Memoize the staking fee calculation const stakingFeeSat = useMemo(() => { if ( @@ -423,34 +319,6 @@ export const Staking = () => { handleFeedbackModal("cancel"); }; - const showOverflowWarning = (overflow: OverflowProperties) => { - if (overflow.isHeightCap) { - return ( - - ); - } else { - return ( - - ); - } - }; - const handleCloseFeedbackModal = () => { if (feedbackModal.type === "success") { setSuccessFeedbackModalOpened(true); @@ -460,24 +328,6 @@ export const Staking = () => { setFeedbackModal({ type: null, isOpen: false }); }; - const showApproachingCapWarning = () => { - if (!overflow.approchingCapRange) { - return; - } - if (overflow.isHeightCap) { - return ( -

- Staking window is closing. Your stake may overflow! -

- ); - } - return ( -

- Staking cap is filling up. Your stake may overflow! -

- ); - }; - const hasError = disabled || hasMempoolFeeRatesError; const renderStakingForm = () => { @@ -500,45 +350,18 @@ export const Staking = () => { else if (isLoading || areMempoolFeeRatesLoading) { return ; } - // Staking has not started yet - else if (isBlockHeightUnderActivation) { - return ( - - ); - } - // Staking params upgrading - else if (isUpgrading) { - return ( - - ); - } - // Staking cap reached - else if (overflow.overTheCapRange) { - return showOverflowWarning(overflow); - } // Staking form else { + const stakingParams = params?.bbnStakingParams.latestVersion; + if (!stakingParams) { + throw new Error("Staking params not loaded"); + } const { minStakingAmountSat, maxStakingAmountSat, minStakingTimeBlocks, maxStakingTimeBlocks, unbondingTime, - confirmationDepth, unbondingFeeSat, } = stakingParams; @@ -576,7 +399,7 @@ export const Staking = () => { @@ -597,7 +420,6 @@ export const Staking = () => { /> )} - {showApproachingCapWarning()} { void; open: () => void; - getSigningStargateClient(): Promise; + signingStargateClient: SigningStargateClient | undefined; } -const getSigningStargateClientDefault = async () => { - throw new Error("Not initialized"); -}; - const CosmosWalletContext = createContext({ bech32Address: "", connected: false, disconnect: () => {}, open: () => {}, - getSigningStargateClient: getSigningStargateClientDefault, + signingStargateClient: undefined, }); export const CosmosWalletProvider = ({ children }: PropsWithChildren) => { - const [cosmosWalletProvider, setCosmosWalletProvider] = useState(); + const [cosmosWalletProvider, setCosmosWalletProvider] = useState< + CosmosProvider | undefined + >(); const [cosmosBech32Address, setCosmosBech32Address] = useState(""); - + const [signingStargateClient, setSigningStargateClient] = useState< + SigningStargateClient | undefined + >(); const { showError, captureError } = useError(); const { open, isConnected, providers } = useWalletConnection(); const cosmosDisconnect = useCallback(() => { setCosmosWalletProvider(undefined); setCosmosBech32Address(""); + setSigningStargateClient(undefined); }, []); const connectCosmos = useCallback(async () => { @@ -53,9 +55,10 @@ export const CosmosWalletProvider = ({ children }: PropsWithChildren) => { try { await providers.cosmosProvider.connectWallet(); const address = await providers.cosmosProvider.getAddress(); - const registry = getBbnRegistry(); - await providers.cosmosProvider.getSigningStargateClient({ registry }); - + const client = await providers.cosmosProvider.getSigningStargateClient({ + registry: getBbnRegistry(), + }); + setSigningStargateClient(client); setCosmosWalletProvider(providers.cosmosProvider); setCosmosBech32Address(address); } catch (error: any) { @@ -73,12 +76,19 @@ export const CosmosWalletProvider = ({ children }: PropsWithChildren) => { const cosmosContextValue = useMemo( () => ({ bech32Address: cosmosBech32Address, - connected: Boolean(cosmosWalletProvider), + connected: + Boolean(cosmosWalletProvider) && Boolean(signingStargateClient), disconnect: cosmosDisconnect, open, - getSigningStargateClient: cosmosWalletProvider?.getSigningStargateClient, + signingStargateClient, }), - [cosmosBech32Address, cosmosWalletProvider, cosmosDisconnect, open], + [ + cosmosBech32Address, + cosmosWalletProvider, + cosmosDisconnect, + open, + signingStargateClient, + ], ); useEffect(() => { diff --git a/src/app/context/wallet/WalletConnectionProvider.tsx b/src/app/context/wallet/WalletConnectionProvider.tsx index 251176be..9c8e36fe 100644 --- a/src/app/context/wallet/WalletConnectionProvider.tsx +++ b/src/app/context/wallet/WalletConnectionProvider.tsx @@ -44,6 +44,9 @@ export const WalletConnectionProvider = ({ children }: PropsWithChildren) => { type: "cosmos", network: "devnet-4", modularData: keplrRegistry, + backendUrls: { + rpcRrl: "https://rpc.devnet.babylonlabs.io", + }, logo: "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/bbn-dev/chain.png", }, ]} diff --git a/src/app/hooks/api/useParams.ts b/src/app/hooks/api/useParams.ts index 5e8447bd..45f6d03f 100644 --- a/src/app/hooks/api/useParams.ts +++ b/src/app/hooks/api/useParams.ts @@ -5,11 +5,9 @@ import { Params } from "@/app/types/params"; export const PARAMS_KEY = "PARAMS"; export function useParams({ enabled = true }: { enabled?: boolean } = {}) { - const data = useAPIQuery({ + return useAPIQuery({ queryKey: [PARAMS_KEY], queryFn: getParams, enabled, }); - - return data; } diff --git a/src/app/hooks/services/useEoiCreationService.tsx b/src/app/hooks/services/useEoiCreationService.tsx new file mode 100644 index 00000000..03b5a567 --- /dev/null +++ b/src/app/hooks/services/useEoiCreationService.tsx @@ -0,0 +1,244 @@ +import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; +import { + BTCSigType, + ProofOfPossessionBTC, +} from "@babylonlabs-io/babylon-proto-ts/dist/generated/babylon/btcstaking/v1/pop"; +import { Staking } from "@babylonlabs-io/btc-staking-ts"; +import { fromBech32 } from "@cosmjs/encoding"; +import { Psbt } from "bitcoinjs-lib"; +import { useCallback } from "react"; + +import { useBTCWallet } from "@/app/context/wallet/BTCWalletProvider"; +import { useCosmosWallet } from "@/app/context/wallet/CosmosWalletProvider"; +import { useAppState } from "@/app/state"; +import { + clearTxSignatures, + extractSchnorrSignaturesFromTransaction, + uint8ArrayToHex, +} from "@/utils/delegations"; + +import { useParams } from "../api/useParams"; + +export interface BtcStakingInputs { + finalityProviderPublicKey: string; + stakingAmountSat: number; + stakingTimeBlocks: number; + feeRate: number; +} + +export enum SigningStep { + STAKING_SLASHING = "staking_slashing", + UNBONDING_SLASHING = "unbonding_slashing", + PROOF_OF_POSSESSION = "proof_of_possession", + SEND_BBN = "send_bbn", +} + +export const useEoiCreationService = () => { + const { availableUTXOs: inputUTXOs } = useAppState(); + const { + connected: cosmosConnected, + bech32Address, + signingStargateClient, + } = useCosmosWallet(); + const { + connected: btcConnected, + signPsbt, + publicKeyNoCoord, + address, + signMessage, + network: btcNetwork, + } = useBTCWallet(); + + const { data: params } = useParams(); + + const createDelegationEoi = useCallback( + async ( + btcInput: BtcStakingInputs, + signingCallback: (step: SigningStep) => Promise, + ) => { + const stakingParams = params?.bbnStakingParams.latestVersion; + if (!stakingParams) { + throw new Error("Staking params not loaded"); + } + // Perform initial validation + if ( + !cosmosConnected || + !btcConnected || + !btcNetwork || + !signingStargateClient + ) { + throw new Error("Wallet not connected"); + } + if (!params) { + throw new Error("Staking params not loaded"); + } + if (!btcInput.finalityProviderPublicKey) { + throw new Error("Finality provider not selected"); + } + if (!btcInput.stakingAmountSat) { + throw new Error("Staking amount not set"); + } + if (!btcInput.stakingTimeBlocks) { + throw new Error("Staking time not set"); + } + if (!inputUTXOs || inputUTXOs.length === 0) { + throw new Error("No input UTXOs"); + } + if (!btcInput.feeRate) { + throw new Error("Fee rate not set"); + } + + const staking = new Staking( + btcNetwork, + { + address, + publicKeyNoCoordHex: publicKeyNoCoord, + }, + stakingParams, + btcInput.finalityProviderPublicKey, + btcInput.stakingTimeBlocks, + ); + + // Create and sign staking transaction + const { psbt: stakingPsbt } = staking.createStakingTransaction( + btcInput.stakingAmountSat, + inputUTXOs, + btcInput.feeRate, + ); + // TODO: This is temporary solution until we have + // https://github.com/babylonlabs-io/btc-staking-ts/issues/40 + const signedStakingPsbtHex = await signPsbt(stakingPsbt.toHex()); + const stakingTx = Psbt.fromHex(signedStakingPsbtHex).extractTransaction(); + const cleanedStakingTx = clearTxSignatures(stakingTx); + + // Create and sign unbonding transactionst + const { psbt: unbondingPsbt } = + staking.createUnbondingTransaction(cleanedStakingTx); + // TODO: This is temporary solution until we have + // https://github.com/babylonlabs-io/btc-staking-ts/issues/40 + const signedUnbondingPsbtHex = await signPsbt(unbondingPsbt.toHex()); + const unbondingTx = Psbt.fromHex( + signedUnbondingPsbtHex, + ).extractTransaction(); + const cleanedUnbondingTx = clearTxSignatures(unbondingTx); + + // Create slashing transactions and extract signatures + const { psbt: slashingPsbt } = + staking.createStakingOutputSlashingTransaction(cleanedStakingTx); + const signedSlashingPsbtHex = await signPsbt(slashingPsbt.toHex()); + const signedSlashingTx = Psbt.fromHex( + signedSlashingPsbtHex, + ).extractTransaction(); + const slashingSig = + extractSchnorrSignaturesFromTransaction(signedSlashingTx); + if (!slashingSig) { + throw new Error( + "No signature found in the staking output slashing PSBT", + ); + } + await signingCallback(SigningStep.STAKING_SLASHING); + + const { psbt: unbondingSlashingPsbt } = + staking.createUnbondingOutputSlashingTransaction(unbondingTx); + const signedUnbondingSlashingPsbtHex = await signPsbt( + unbondingSlashingPsbt.toHex(), + ); + const signedUnbondingSlashingTx = Psbt.fromHex( + signedUnbondingSlashingPsbtHex, + ).extractTransaction(); + const unbondingSignatures = extractSchnorrSignaturesFromTransaction( + signedUnbondingSlashingTx, + ); + if (!unbondingSignatures) { + throw new Error( + "No signature found in the unbonding output slashing PSBT", + ); + } + await signingCallback(SigningStep.UNBONDING_SLASHING); + + // Create Proof of Possession + const bech32AddressHex = uint8ArrayToHex(fromBech32(bech32Address).data); + const signedBbnAddress = await signMessage(bech32AddressHex, "ecdsa"); + const ecdsaSig = Uint8Array.from( + globalThis.Buffer.from(signedBbnAddress, "base64"), + ); + const proofOfPossession: ProofOfPossessionBTC = { + btcSigType: BTCSigType.ECDSA, + btcSig: ecdsaSig, + }; + await signingCallback(SigningStep.PROOF_OF_POSSESSION); + + // Prepare and send protobuf message + const msg: btcstakingtx.MsgCreateBTCDelegation = + btcstakingtx.MsgCreateBTCDelegation.fromPartial({ + stakerAddr: bech32Address, + pop: proofOfPossession, + btcPk: Uint8Array.from(Buffer.from(publicKeyNoCoord, "hex")), + fpBtcPkList: [ + Uint8Array.from( + Buffer.from(btcInput.finalityProviderPublicKey, "hex"), + ), + ], + stakingTime: btcInput.stakingTimeBlocks, + stakingValue: btcInput.stakingAmountSat, + stakingTx: Uint8Array.from(cleanedStakingTx.toBuffer()), + slashingTx: Uint8Array.from( + Buffer.from(clearTxSignatures(signedSlashingTx).toHex(), "hex"), + ), + delegatorSlashingSig: Uint8Array.from(slashingSig), + // TODO: Confirm with core on the value whether its inclusive or exclusive + unbondingTime: stakingParams.unbondingTime + 1, + unbondingTx: Uint8Array.from(cleanedUnbondingTx.toBuffer()), + unbondingValue: + btcInput.stakingAmountSat - stakingParams.unbondingFeeSat, + unbondingSlashingTx: Uint8Array.from( + Buffer.from( + clearTxSignatures(signedUnbondingSlashingTx).toHex(), + "hex", + ), + ), + delegatorUnbondingSlashingSig: Uint8Array.from(unbondingSignatures), + stakingTxInclusionProof: undefined, + }); + + const protoMsg = { + typeUrl: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", + value: msg, + }; + + // estimate gas + const gasEstimate = await signingStargateClient.simulate( + bech32Address, + [protoMsg], + "estimate fee", + ); + const gasWanted = Math.ceil(gasEstimate * 1.2); + const fee = { + amount: [{ denom: "ubbn", amount: (gasWanted * 0.01).toFixed(0) }], + gas: gasWanted.toString(), + }; + // sign it + await signingStargateClient.signAndBroadcast( + bech32Address, + [protoMsg], + fee, + ); + await signingCallback(SigningStep.SEND_BBN); + }, + [ + cosmosConnected, + btcConnected, + btcNetwork, + params, + inputUTXOs, + address, + publicKeyNoCoord, + signPsbt, + signMessage, + bech32Address, + signingStargateClient, + ], + ); + + return { createDelegationEoi }; +}; diff --git a/src/utils/delegations/index.ts b/src/utils/delegations/index.ts new file mode 100644 index 00000000..2767cafd --- /dev/null +++ b/src/utils/delegations/index.ts @@ -0,0 +1,46 @@ +import { Transaction } from "bitcoinjs-lib"; + +/** + * Clears the signatures from a transaction. + * @param tx - The transaction to clear the signatures from. + * @returns The transaction with the signatures cleared. + */ +export const clearTxSignatures = (tx: Transaction): Transaction => { + tx.ins.forEach((input) => { + input.script = Buffer.alloc(0); + input.witness = []; + }); + return tx; +}; + +/** + * Extracts the first valid Schnorr signature from a signed transaction. + * @param singedTransaction - The signed transaction. + * @returns The first valid Schnorr signature or undefined if no valid signature is found. + */ +export const extractSchnorrSignaturesFromTransaction = ( + singedTransaction: Transaction, +): Buffer | undefined => { + // Loop through each input to extract the witness signature + for (const input of singedTransaction.ins) { + if (input.witness && input.witness.length > 0) { + const schnorrSignature = input.witness[0]; + + // Check that it's a 64-byte Schnorr signature + if (schnorrSignature.length === 64) { + return schnorrSignature; // Return the first valid signature found + } + } + } + return undefined; +}; +/** + * Converts a Uint8Array to a hexadecimal string. + * @param uint8Array - The Uint8Array to convert. + * @returns The hexadecimal string. + */ +export const uint8ArrayToHex = (uint8Array: Uint8Array): string => { + return Array.from(uint8Array) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +}; diff --git a/src/utils/wallet/bbnRegistry.ts b/src/utils/wallet/bbnRegistry.ts index 4509d834..207a5a0a 100644 --- a/src/utils/wallet/bbnRegistry.ts +++ b/src/utils/wallet/bbnRegistry.ts @@ -1,6 +1,41 @@ -import { Registry } from "@cosmjs/proto-signing"; +import { btcstakingtx } from "@babylonlabs-io/babylon-proto-ts"; +import { MessageFns } from "@babylonlabs-io/babylon-proto-ts/dist/generated/google/protobuf/any"; +import { GeneratedType, Registry } from "@cosmjs/proto-signing"; +// Define the structure of each proto to register +type ProtoToRegister = { + typeUrl: string; + messageType: MessageFns; +}; + +// List of protos to register in the registry +const protosToRegister: ProtoToRegister[] = [ + { + typeUrl: "/babylon.btcstaking.v1.MsgCreateBTCDelegation", + messageType: btcstakingtx.MsgCreateBTCDelegation, + }, +]; + +// Utility function to create a `GeneratedType` from `MessageFns` +// Temporary workaround until https://github.com/cosmos/cosmjs/issues/1613 is fixed +const createGeneratedType = (messageType: any): GeneratedType => { + return { + encode: messageType.encode.bind(messageType), + decode: messageType.decode.bind(messageType), + fromPartial: (properties?: Partial): T => { + return messageType.fromPartial(properties ?? ({} as T)); + }, + }; +}; + +// Create the registry with the protos to register export const getBbnRegistry = (): Registry => { - // TODO: Implement the BBN registry - return new Registry(); + const registry = new Registry(); + + protosToRegister.forEach((proto) => { + const generatedType = createGeneratedType(proto.messageType); + registry.register(proto.typeUrl, generatedType); + }); + + return registry; };