From 58430617b5f9999e49824db208a8991117c8ee37 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 5 Feb 2024 10:05:58 +0100 Subject: [PATCH] feat: add sendpsbt function --- .../components/lock-screen/lock-screen.tsx | 44 +++++++- .../transaction-form/transaction-form.tsx | 63 ++--------- .../components/walkthrough/walkthrough.tsx | 9 +- src/app/hooks/use-bitcoin.ts | 106 ++++++++++++------ src/app/hooks/use-sign-psbt.ts | 75 ++++++++++--- src/shared/models/bitcoin-network.ts | 53 ++++----- 6 files changed, 211 insertions(+), 139 deletions(-) diff --git a/src/app/components/mint-unmint/components/lock-screen/lock-screen.tsx b/src/app/components/mint-unmint/components/lock-screen/lock-screen.tsx index 44c974ad..55b87e14 100644 --- a/src/app/components/mint-unmint/components/lock-screen/lock-screen.tsx +++ b/src/app/components/mint-unmint/components/lock-screen/lock-screen.tsx @@ -1,9 +1,11 @@ import { useContext, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Button, VStack } from '@chakra-ui/react'; +import { Button, Checkbox, Fade, HStack, Stack, Text, VStack, useToast } from '@chakra-ui/react'; import { VaultCard } from '@components/vault/vault-card'; +import { useSignPSBT } from '@hooks/use-sign-psbt'; import { useVaults } from '@hooks/use-vaults'; +import { BitcoinError } from '@models/error-types'; import { Vault } from '@models/vault'; import { BlockchainContext } from '@providers/blockchain-context-provider'; import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; @@ -15,10 +17,13 @@ interface LockScreenProps { } export function LockScreen({ currentStep }: LockScreenProps): React.JSX.Element { + const toast = useToast(); const dispatch = useDispatch(); const { readyVaults } = useVaults(); const blockchainContext = useContext(BlockchainContext); const bitcoin = blockchainContext?.bitcoin; + const { handleSignTransaction, fundingTransactionSigned, closingTransactionSigned } = + useSignPSBT(bitcoin); const ethereum = blockchainContext?.ethereum; const [isSubmitting, setIsSubmitting] = useState(false); @@ -39,10 +44,17 @@ export function LockScreen({ currentStep }: LockScreenProps): React.JSX.Element try { setIsSubmitting(true); - await bitcoin?.fetchBitcoinContractOfferAndSendToUserWallet(currentVault); + await handleSignTransaction(currentVault.collateral, currentVault.uuid); + setIsSubmitting(false); } catch (error) { setIsSubmitting(false); - throw new Error('Error locking vault'); + toast({ + title: 'Failed to sign transaction', + description: error instanceof BitcoinError ? error.message : '', + status: 'error', + duration: 9000, + isClosable: true, + }); } } @@ -68,6 +80,32 @@ export function LockScreen({ currentStep }: LockScreenProps): React.JSX.Element > Cancel + + + + + + + Funding Transaction + + + + + + Closing Transaction + + + + + ); } diff --git a/src/app/components/mint-unmint/components/transaction-form/transaction-form.tsx b/src/app/components/mint-unmint/components/transaction-form/transaction-form.tsx index e06aa06b..fec9e1e1 100644 --- a/src/app/components/mint-unmint/components/transaction-form/transaction-form.tsx +++ b/src/app/components/mint-unmint/components/transaction-form/transaction-form.tsx @@ -1,20 +1,8 @@ import { useContext, useState } from 'react'; -import { - Button, - Checkbox, - Fade, - FormControl, - FormErrorMessage, - HStack, - Stack, - Text, - VStack, - useToast, -} from '@chakra-ui/react'; +import { Button, FormControl, FormErrorMessage, Text, VStack, useToast } from '@chakra-ui/react'; import { customShiftValue } from '@common/utilities'; -import { useSignPSBT } from '@hooks/use-sign-psbt'; -import { BitcoinError } from '@models/error-types'; +import { EthereumError } from '@models/error-types'; import { BlockchainContext } from '@providers/blockchain-context-provider'; import { Form, Formik } from 'formik'; @@ -30,9 +18,7 @@ const initialValues: TransactionFormValues = { amount: 0.001 }; export function TransactionForm(): React.JSX.Element { const toast = useToast(); const blockchainContext = useContext(BlockchainContext); - const { handleSignTransaction, fundingTransactionSigned, closingTransactionSigned } = useSignPSBT( - blockchainContext?.bitcoin - ); + const ethereum = blockchainContext?.ethereum; const bitcoinPrice = blockchainContext?.bitcoin.bitcoinPrice; const [isSubmitting, setIsSubmitting] = useState(false); @@ -40,13 +26,12 @@ export function TransactionForm(): React.JSX.Element { try { setIsSubmitting(true); const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); - await handleSignTransaction(shiftedBTCDepositAmount); - setIsSubmitting(false); + await ethereum?.setupVault(shiftedBTCDepositAmount); } catch (error) { setIsSubmitting(false); toast({ - title: 'Failed to sign transaction', - description: error instanceof BitcoinError ? error.message : '', + title: 'Failed to create vault', + description: error instanceof EthereumError ? error.message : '', status: 'error', duration: 9000, isClosable: true, @@ -78,42 +63,8 @@ export function TransactionForm(): React.JSX.Element { type={'submit'} isDisabled={Boolean(errors.amount)} > - {fundingTransactionSigned ? 'Sign Closing Transaction' : 'Lock Bitcoin'} + Create Vault - - - - - - - Funding Transaction - - - - - - Closing Transaction - - - - - diff --git a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx index e52befff..7ab2a4d0 100644 --- a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx +++ b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx @@ -25,18 +25,17 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. - Select an amount of dlcBTC you would like to mint and sign the required transactions - in your{' '} + Select an amount of dlcBTC you would like to mint and confirm it in your{' '} - Bitcoin Wallet + Ethereum Wallet . diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index 0f0e0187..8b653cc8 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -2,21 +2,19 @@ import { useEffect, useState } from 'react'; import { BitcoinNetwork, regtest } from '@models/bitcoin-network'; import { BitcoinError } from '@models/error-types'; -import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { hex } from '@scure/base'; import * as btc from '@scure/btc-signer'; -import { concatBytes } from 'micro-packed'; const networkModes = ['mainnet', 'testnet'] as const; -export type NetworkModes = (typeof networkModes)[number]; +type NetworkModes = (typeof networkModes)[number]; -type BitcoinTestnetModes = 'testnet' | 'regtest' | 'signet'; +// type BitcoinTestnetModes = 'testnet' | 'regtest' | 'signet'; -export type BitcoinNetworkModes = NetworkModes | BitcoinTestnetModes; +// type BitcoinNetworkModes = NetworkModes | BitcoinTestnetModes; -export declare enum SignatureHash { +declare enum SignatureHash { ALL = 1, NONE = 2, SINGLE = 3, @@ -77,9 +75,7 @@ interface RpcResponse { } export interface UseBitcoinReturnType { - signAndBroadcastFundingPSBT: ( - btcAmount: number - ) => Promise<{ + signAndBroadcastFundingPSBT: (btcAmount: number) => Promise<{ fundingTransactionID: string; multisigTransaction: btc.P2TROut; userNativeSegwitAddress: string; @@ -88,18 +84,19 @@ export interface UseBitcoinReturnType { signClosingPSBT: ( fundingTransactionID: string, multisigTransaction: btc.P2TROut, + uuid: string, userNativeSegwitAddress: string, btcAmount: number ) => Promise; bitcoinPrice: number; } -export interface SignAndBroadcastFundingPSBTResult { - fundingTransactionID: string; - multisigTransaction: btc.P2TROut; - userNativeSegwitAddress: string; - btcAmount: number; -} +// interface SignAndBroadcastFundingPSBTResult { +// fundingTransactionID: string; +// multisigTransaction: btc.P2TROut; +// userNativeSegwitAddress: string; +// btcAmount: number; +// } const ELECTRUM_API_URL = 'https://devnet.dlc.link/electrs'; @@ -149,6 +146,13 @@ export function useBitcoin(): UseBitcoinReturnType { return utxos; } + async function getAttestorPublicKey(): Promise { + const response = await fetch('http://localhost:3000/publickey'); + const attestorPublicKey = await response.text(); + console.log('attestorPublicKey', attestorPublicKey); + return attestorPublicKey; + } + function createMultisigTransactionAndAddress( userPublicKey: Uint8Array, attestorPublicKey: Uint8Array, @@ -193,19 +197,37 @@ export function useBitcoin(): UseBitcoinReturnType { return fundingPSBT; } - function getFundingTransactionID(fundingTransaction: Uint8Array): string { - const sha256x2 = (...msgs: Uint8Array[]) => sha256(sha256(concatBytes(...msgs))); - const fundingTransactionID = hex.encode(sha256x2(fundingTransaction).reverse()); - return fundingTransactionID; - } + // function getFundingTransactionID(fundingTransaction: Uint8Array): string { + // const sha256x2 = (...msgs: Uint8Array[]) => sha256(sha256(concatBytes(...msgs))); + // const fundingTransactionID = hex.encode(sha256x2(fundingTransaction).reverse()); + // return fundingTransactionID; + // } - function createClosingTransaction( + async function sendPSBT( + closingPSBT: string, + uuid: string, + userNativeSegwitAddress: string + ): Promise { + setBTCNetwork(regtest); + try { + const response = await fetch('http://localhost:3000/create-psbt-event', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, + body: JSON.stringify({ closingPSBT, uuid, userNativeSegwitAddress }), + }); + console.log('response', response); + } catch (error) { + throw new BitcoinError(`Error sending PSBT: ${error}`); + } + } + async function createClosingTransaction( fundingTransactionID: string, multisigTransaction: any, userNativeSegwitAddress: string, + uuid: string, btcAmount: number, btcNetwork: BitcoinNetwork - ): Uint8Array { + ): Promise { const closingTransaction = new btc.Transaction(); const fundingInput = { txid: hexToBytes(fundingTransactionID), @@ -216,6 +238,7 @@ export function useBitcoin(): UseBitcoinReturnType { closingTransaction.addInput(fundingInput); closingTransaction.addOutputAddress(userNativeSegwitAddress, BigInt(btcAmount), btcNetwork); const closingPSBT = closingTransaction.toPSBT(); + await sendPSBT(bytesToHex(closingPSBT), uuid, userNativeSegwitAddress); return closingPSBT; } @@ -235,7 +258,7 @@ export function useBitcoin(): UseBitcoinReturnType { utxos: any[], btcAmount: number, btcNetwork: BitcoinNetwork - ): Promise<{ fundingTransaction: Uint8Array; fundingTransactionHex: string }> { + ): Promise<{ fundingTransactionHex: string; fundingTransactionID: string }> { const fundingTransaction = createFundingTransaction( multisigAddress, userChangeAddress, @@ -243,21 +266,40 @@ export function useBitcoin(): UseBitcoinReturnType { btcAmount, btcNetwork ); - const fundingTransactionHex = await signPSBT(fundingTransaction, true); - return { fundingTransaction, fundingTransactionHex }; + const fundingTransactionHex = await signPSBT(fundingTransaction, false); + const transaction = btc.Transaction.fromPSBT(hexToBytes(fundingTransactionHex)); + transaction.finalize(); + + let fundingTransactionID = ''; + await fetch(`${ELECTRUM_API_URL}/tx`, { + method: 'POST', + body: bytesToHex(transaction.extract()), + }).then(async response => { + fundingTransactionID = await response.text(); + }); + return { fundingTransactionHex, fundingTransactionID }; } async function handleClosingTransaction( fundingTransactionID: string, multisigTransaction: btc.P2TROut, userAddress: string, + uuid: string, btcAmount: number, btcNetwork: BitcoinNetwork ): Promise { - const closingTransaction = createClosingTransaction( + console.log('fundingTransactionID', fundingTransactionID); + console.log('multisigTransaction', multisigTransaction); + console.log('userAddress', userAddress); + console.log('uuid', uuid); + console.log('btcAmount', btcAmount); + console.log('btcNetwork', btcNetwork); + + const closingTransaction = await createClosingTransaction( fundingTransactionID, multisigTransaction, userAddress, + uuid, btcAmount, btcNetwork ); @@ -265,9 +307,7 @@ export function useBitcoin(): UseBitcoinReturnType { return closingTransactionHex; } - async function signAndBroadcastFundingPSBT( - btcAmount: number - ): Promise<{ + async function signAndBroadcastFundingPSBT(btcAmount: number): Promise<{ fundingTransactionID: string; multisigTransaction: btc.P2TROut; userNativeSegwitAddress: string; @@ -279,7 +319,7 @@ export function useBitcoin(): UseBitcoinReturnType { const userTaprootAddress = userAddresses[1] as BitcoinTaprootAddress; const userPublicKey = userTaprootAddress.tweakedPublicKey; - const attestorPublicKey = 'a27d8d7e1976c7ffaea08ead4aec592da663bcdda75d49ff4bf92dfcb508476e'; + const attestorPublicKey = await getAttestorPublicKey(); // const preImage = hexToBytes('107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f'); // const preImageHash = hexToBytes( @@ -293,7 +333,7 @@ export function useBitcoin(): UseBitcoinReturnType { btcNetwork ); - const { fundingTransaction, fundingTransactionHex } = await handleFundingTransaction( + const { fundingTransactionHex, fundingTransactionID } = await handleFundingTransaction( multisigAddress, userNativeSegwitAddress, userUTXOs, @@ -302,14 +342,13 @@ export function useBitcoin(): UseBitcoinReturnType { ); console.log('fundingTransactionHex', fundingTransactionHex); - const fundingTransactionID = getFundingTransactionID(fundingTransaction); - return { fundingTransactionID, multisigTransaction, userNativeSegwitAddress, btcAmount }; } async function signClosingPSBT( fundingTransactionID: string, multisigTransaction: btc.P2TROut, + uuid: string, userNativeSegwitAddress: string, btcAmount: number ): Promise { @@ -319,6 +358,7 @@ export function useBitcoin(): UseBitcoinReturnType { fundingTransactionID, multisigTransaction, userNativeSegwitAddress, + uuid, btcAmount, btcNetwork ); diff --git a/src/app/hooks/use-sign-psbt.ts b/src/app/hooks/use-sign-psbt.ts index 3c1bf1da..a13f998e 100644 --- a/src/app/hooks/use-sign-psbt.ts +++ b/src/app/hooks/use-sign-psbt.ts @@ -1,12 +1,17 @@ import { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { customShiftValue } from '@common/utilities'; import { BitcoinError } from '@models/error-types'; import * as btc from '@scure/btc-signer'; +import { RootState } from '@store/index'; +import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; +import { vaultActions } from '@store/slices/vault/vault.actions'; import { UseBitcoinReturnType } from './use-bitcoin'; -export interface UseSignPSBTReturnType { - handleSignTransaction: (btcAmount: number) => Promise; +interface UseSignPSBTReturnType { + handleSignTransaction: (btcAmount: number, vaultUUID: string) => Promise; fundingTransactionSigned: boolean; closingTransactionSigned: boolean; } @@ -15,7 +20,10 @@ export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTRetur if (!useBitcoin) throw new Error('useBitcoin must be set before useSignPSBT can be used'); const { signAndBroadcastFundingPSBT, signClosingPSBT } = useBitcoin; + const { network } = useSelector((state: RootState) => state.account); + const dispatch = useDispatch(); const [btcAmount, setBTCAmount] = useState(); + const [vaultUUID, setVaultUUID] = useState(); const [fundingTransactionSigned, setFundingTransactionSigned] = useState(false); const [closingTransactionSigned, setClosingTransactionSigned] = useState(false); const [fundingTransactionID, setFundingTransactionID] = useState(); @@ -23,24 +31,39 @@ export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTRetur const [userNativeSegwitAddress, setUserNativeSegwitAddress] = useState(); useEffect(() => { - if ( - fundingTransactionSigned && - btcAmount && - fundingTransactionID && - multisigTransaction && - userNativeSegwitAddress - ) { - handleSignClosingTransaction(); - } + const signClosingTransaction = async () => { + if ( + fundingTransactionSigned && + btcAmount && + vaultUUID && + network && + fundingTransactionID && + multisigTransaction && + userNativeSegwitAddress + ) { + await handleSignClosingTransaction(); + dispatch( + vaultActions.setVaultToFunding({ + vaultUUID, + fundingTX: fundingTransactionID, + networkID: network.id, + }) + ); + dispatch(mintUnmintActions.setMintStep([2, vaultUUID])); + } + }; + signClosingTransaction(); }, [ fundingTransactionSigned, btcAmount, fundingTransactionID, multisigTransaction, userNativeSegwitAddress, + network, + vaultUUID, ]); - async function handleSignFundingTransaction(btcAmount: number) { + async function handleSignFundingTransaction(btcAmount: number): Promise { try { const { fundingTransactionID, multisigTransaction, userNativeSegwitAddress } = await signAndBroadcastFundingPSBT(btcAmount); @@ -49,6 +72,7 @@ export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTRetur setMultisigTransaction(multisigTransaction); setUserNativeSegwitAddress(userNativeSegwitAddress); setFundingTransactionSigned(true); + return fundingTransactionID; } catch (error) { throw new BitcoinError(`Error signing funding transaction: ${error}`); } @@ -56,7 +80,13 @@ export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTRetur async function handleSignClosingTransaction() { try { - if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) { + if ( + !fundingTransactionID || + !multisigTransaction || + !userNativeSegwitAddress || + !btcAmount || + !vaultUUID + ) { throw new Error( 'Funding transaction must be signed before closing transaction can be signed' ); @@ -64,6 +94,7 @@ export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTRetur await signClosingPSBT( fundingTransactionID, multisigTransaction, + vaultUUID, userNativeSegwitAddress, btcAmount ); @@ -73,12 +104,24 @@ export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTRetur } } - async function handleSignTransaction(btcAmount: number) { - setBTCAmount(btcAmount); + async function handleSignTransaction(btcAmount: number, vaultUUID: string) { + const shiftedBTCDepositAmount = customShiftValue(btcAmount, 8, false); + setBTCAmount(shiftedBTCDepositAmount); + setVaultUUID(vaultUUID); if (!fundingTransactionSigned) { - await handleSignFundingTransaction(btcAmount); + await handleSignFundingTransaction(shiftedBTCDepositAmount); } else { await handleSignClosingTransaction(); + if (fundingTransactionID && network && vaultUUID) { + dispatch( + vaultActions.setVaultToFunding({ + vaultUUID, + fundingTX: fundingTransactionID, + networkID: network.id, + }) + ); + dispatch(mintUnmintActions.setMintStep([2, vaultUUID])); + } } } diff --git a/src/shared/models/bitcoin-network.ts b/src/shared/models/bitcoin-network.ts index 8fbb54ed..9aa42b6e 100644 --- a/src/shared/models/bitcoin-network.ts +++ b/src/shared/models/bitcoin-network.ts @@ -14,19 +14,20 @@ export interface BitcoinNetwork { versionBytes: number; } -export const bitcoin: BitcoinNetwork = { - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bc', - bip32: { - public: 0x0488b21e, - private: 0x0488ade4, - }, - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80, - bytes: 21, - versionBytes: 1, -}; +// export const bitcoin: BitcoinNetwork = { +// messagePrefix: '\x18Bitcoin Signed Message:\n', +// bech32: 'bc', +// bip32: { +// public: 0x0488b21e, +// private: 0x0488ade4, +// }, +// pubKeyHash: 0x00, +// scriptHash: 0x05, +// wif: 0x80, +// bytes: 21, +// versionBytes: 1, +// }; + export const regtest: BitcoinNetwork = { messagePrefix: '\x18Bitcoin Signed Message:\n', bech32: 'bcrt', @@ -40,16 +41,16 @@ export const regtest: BitcoinNetwork = { bytes: 21, versionBytes: 1, }; -export const testnet: BitcoinNetwork = { - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tb', - bip32: { - public: 0x043587cf, - private: 0x04358394, - }, - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef, - bytes: 21, - versionBytes: 1, -}; +// export const testnet: BitcoinNetwork = { +// messagePrefix: '\x18Bitcoin Signed Message:\n', +// bech32: 'tb', +// bip32: { +// public: 0x043587cf, +// private: 0x04358394, +// }, +// pubKeyHash: 0x6f, +// scriptHash: 0xc4, +// wif: 0xef, +// bytes: 21, +// versionBytes: 1, +// };