From 6e0e2252073f884f2eeb4affd8c93c7fa2f09bb4 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 31 Jan 2024 09:30:44 +0100 Subject: [PATCH 01/14] feat: add psbt support logic --- package-lock.json | 157 +++++++ package.json | 6 + .../transaction-form/transaction-form.tsx | 11 +- src/app/hooks/use-bitcoin.ts | 442 +++++++++++++++--- src/shared/models/bitcoin-network.ts | 55 +++ 5 files changed, 601 insertions(+), 70 deletions(-) create mode 100644 src/shared/models/bitcoin-network.ts diff --git a/package-lock.json b/package-lock.json index 62257683..d322bcb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,14 @@ "@emotion/styled": "^11.11.0", "@fontsource/poppins": "^5.0.8", "@ls-lint/ls-lint": "^2.2.2", + "@noble/hashes": "^1.3.3", "@reduxjs/toolkit": "^1.9.7", + "@scure/base": "^1.1.5", + "@scure/bip32": "^1.3.3", + "@scure/btc-signer": "^1.2.1", "@trivago/prettier-plugin-sort-imports": "^4.2.1", "@types/chrome": "^0.0.248", + "bitcoinjs-lib": "^6.1.5", "concurrently": "^8.2.2", "decimal.js": "^10.4.3", "dotenv": "^16.3.1", @@ -27,6 +32,7 @@ "eslint-plugin-react": "^7.33.2", "ethers": "5.7.2", "formik": "^2.4.5", + "micro-packed": "^0.5.1", "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -2538,6 +2544,28 @@ "ls-lint": "bin/cli.js" } }, + "node_modules/@noble/curves": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", + "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "dependencies": { + "@noble/hashes": "1.3.3" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -2630,6 +2658,41 @@ "darwin" ] }, + "node_modules/@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.3.tgz", + "integrity": "sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==", + "dependencies": { + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.4" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/btc-signer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/btc-signer/-/btc-signer-1.2.1.tgz", + "integrity": "sha512-/Zle18/aWhYDBuBeXGDGJTdo0/LKpQhU8ETBJeWABCQkbk0QHCFCinidTiz9hdQFfh0HtasPGq5p6EodVCfEew==", + "dependencies": { + "@noble/curves": "~1.3.0", + "@noble/hashes": "~1.3.3", + "@scure/base": "~1.1.5", + "micro-packed": "~0.5.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "license": "MIT" @@ -3443,10 +3506,44 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, "node_modules/bech32": { "version": "1.1.4", "license": "MIT" }, + "node_modules/bip174": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.1.1.tgz", + "integrity": "sha512-mdFV5+/v0XyNYXjBS6CQPLo9ekCx4gtKZFnJm5PMto7Fs9hTTDpkkzOB7/FtluRI6JbUUAu+snTYfJRgHLZbZQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.5.tgz", + "integrity": "sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.1", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bitcoinjs-lib/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "node_modules/bn.js": { "version": "5.2.1", "license": "MIT" @@ -3504,6 +3601,23 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, "node_modules/cac": { "version": "6.7.14", "license": "MIT", @@ -5452,6 +5566,17 @@ "node": ">= 8" } }, + "node_modules/micro-packed": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.5.1.tgz", + "integrity": "sha512-VjBHcsMAVfivjCZPnqAEEkcihPBbNd39KLEMH76ksL3ORKSZE04gkrtsAmXtaTor67PmTO5h0Rq9+j3PA4zNrw==", + "dependencies": { + "@scure/base": "~1.1.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/micromatch": { "version": "4.0.5", "license": "MIT", @@ -6333,6 +6458,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "license": "MIT", @@ -6803,6 +6947,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typeforce": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", + "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" + }, "node_modules/typescript": { "version": "5.3.2", "license": "Apache-2.0", @@ -6917,6 +7066,14 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/varuint-bitcoin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-1.1.2.tgz", + "integrity": "sha512-4EVb+w4rx+YfVM32HQX42AbbT7/1f5zwAYhIujKXKk8NQK+JfRVl3pqT3hjNn/L+RstigmGGKVwHA/P0wgITZw==", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, "node_modules/vite": { "version": "4.5.0", "dev": true, diff --git a/package.json b/package.json index 4e21eddd..605dc358 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,14 @@ "@emotion/styled": "^11.11.0", "@fontsource/poppins": "^5.0.8", "@ls-lint/ls-lint": "^2.2.2", + "@noble/hashes": "^1.3.3", "@reduxjs/toolkit": "^1.9.7", + "@scure/base": "^1.1.5", + "@scure/bip32": "^1.3.3", + "@scure/btc-signer": "^1.2.1", "@trivago/prettier-plugin-sort-imports": "^4.2.1", "@types/chrome": "^0.0.248", + "bitcoinjs-lib": "^6.1.5", "concurrently": "^8.2.2", "decimal.js": "^10.4.3", "dotenv": "^16.3.1", @@ -37,6 +42,7 @@ "eslint-plugin-react": "^7.33.2", "ethers": "5.7.2", "formik": "^2.4.5", + "micro-packed": "^0.5.1", "prettier": "^3.0.3", "react": "^18.2.0", "react-dom": "^18.2.0", 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 fec9e1e1..33f2bc95 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,7 +1,6 @@ import { useContext, useState } from 'react'; import { Button, FormControl, FormErrorMessage, Text, VStack, useToast } from '@chakra-ui/react'; -import { customShiftValue } from '@common/utilities'; import { EthereumError } from '@models/error-types'; import { BlockchainContext } from '@providers/blockchain-context-provider'; import { Form, Formik } from 'formik'; @@ -18,15 +17,15 @@ const initialValues: TransactionFormValues = { amount: 0.001 }; export function TransactionForm(): React.JSX.Element { const toast = useToast(); const blockchainContext = useContext(BlockchainContext); - const ethereum = blockchainContext?.ethereum; const bitcoinPrice = blockchainContext?.bitcoin.bitcoinPrice; const [isSubmitting, setIsSubmitting] = useState(false); - async function handleSetup(btcDepositAmount: number) { + async function handleSetup() { try { setIsSubmitting(true); - const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); - await ethereum?.setupVault(shiftedBTCDepositAmount); + // const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); + // await ethereum?.setupVault(shiftedBTCDepositAmount); + blockchainContext?.bitcoin.test(); } catch (error) { setIsSubmitting(false); toast({ @@ -44,7 +43,7 @@ export function TransactionForm(): React.JSX.Element { { - await handleSetup(values.amount); + await handleSetup(); }} > {({ handleSubmit, errors, touched, values }) => ( diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index c9503cbc..4b5c2a82 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -1,22 +1,94 @@ import { useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { BitcoinNetwork, regtest } from '@models/bitcoin-network'; import { BitcoinError } from '@models/error-types'; -import { Vault } from '@models/vault'; -import { RootState } from '@store/index'; -import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; -import { vaultActions } from '@store/slices/vault/vault.actions'; +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'; -import { useEndpoints } from './use-endpoints'; +const networkModes = ['mainnet', 'testnet'] as const; + +export type NetworkModes = (typeof networkModes)[number]; + +type BitcoinTestnetModes = 'testnet' | 'regtest' | 'signet'; + +export type BitcoinNetworkModes = NetworkModes | BitcoinTestnetModes; + +export declare enum SignatureHash { + ALL = 1, + NONE = 2, + SINGLE = 3, + ALL_ANYONECANPAY = 129, + NONE_ANYONECANPAY = 130, + SINGLE_ANYONECANPAY = 131, +} +interface SignPsbtRequestParams { + hex: string; + allowedSighash?: SignatureHash[]; + signAtIndex?: number | number[]; + network?: NetworkModes; // default is user's current network + account?: number; // default is user's current account + broadcast?: boolean; // default is false - finalize/broadcast tx +} + +interface TransactionStatus { + confirmed: boolean; + block_height: number; + block_hash: string; + block_time: number; +} + +interface UTXO { + txid: string; + vout: number; + status: TransactionStatus; + value: number; +} + +interface BitcoinNativeSegwitAddress { + address: string; + derivationPath: string; + publicKey: string; + symbol: string; + type: string; +} + +interface BitcoinTaprootAddress extends BitcoinNativeSegwitAddress { + type: 'p2tr'; + tweakedPublicKey: string; +} + +interface StacksAddress { + address: string; + symbol: string; +} + +interface BitcoinAddresses { + nativeSegwit: BitcoinNativeSegwitAddress; + taproot: BitcoinTaprootAddress; +} + +type Address = BitcoinNativeSegwitAddress | BitcoinTaprootAddress | StacksAddress; +interface RpcResult { + addresses: Address[]; +} + +interface RpcResponse { + id: string; + jsonrpc: string; + result: RpcResult; +} export interface UseBitcoinReturnType { - fetchBitcoinContractOfferAndSendToUserWallet: (vault: Vault) => Promise; + test: () => Promise; bitcoinPrice: number; } +const ELECTRUM_API_URL = 'https://devnet.dlc.link/electrs'; + export function useBitcoin(): UseBitcoinReturnType { - const dispatch = useDispatch(); - const { routerWalletURL } = useEndpoints(); const [bitcoinPrice, setBitcoinPrice] = useState(0); useEffect(() => { @@ -25,70 +97,312 @@ export function useBitcoin(): UseBitcoinReturnType { }; getBitcoinPrice(); }, []); - const { network } = useSelector((state: RootState) => state.account); - function createURLParams(bitcoinContractOffer: any) { - if (!routerWalletURL) { - throw new BitcoinError('Router wallet URL is undefined'); - } + async function getBitcoinAddresses(): Promise { + console.log('getBitcoinAddresses'); + const rpcResponse: RpcResponse = await window.btc?.request('getAddresses'); + const userAddresses = rpcResponse.result.addresses; + return userAddresses; + } - const counterPartyWalletDetails = { - counterpartyWalletURL: routerWalletURL, - counterpartyWalletName: 'DLC.Link', - counterpartyWalletIcon: - 'https://dlc-public-assets.s3.amazonaws.com/DLC.Link_logo_icon_color.svg', + async function gatherUTXOs(bitcoinNativeSegwitAddress: BitcoinNativeSegwitAddress): Promise { + console.log('gatherUTXOs'); + const response = await fetch( + `${ELECTRUM_API_URL}/address/${bitcoinNativeSegwitAddress.address}/utxo` + ); + const allUTXOs = await response.json(); + const spend = btc.p2wpkh(bitcoinNativeSegwitAddress.publicKey, regtest); + + const utxos = await Promise.all( + allUTXOs.map(async (utxo: UTXO) => { + const txHex = await (await fetch(`${ELECTRUM_API_URL}/tx/${utxo.txid}/hex`)).text(); + return { + ...spend, + txid: utxo.txid, + index: utxo.vout, + value: utxo.value, + nonWitnessUtxo: hex.decode(txHex), + // script: utxo.scriptpubkey, //do i need to handle when it is witness? how? + }; + }) + ); + + return utxos; + } + + function createTaprootTree( + userTimeoutPublicKey: Uint8Array, + userPublicKey: Uint8Array, + attestorsPublicKey: Uint8Array, + hash: Uint8Array + ): btc.TaprootScriptTree { + console.log('createTaprootTree'); + const timeoutScript = new Uint8Array([ + 0x02, + 144, + 0x00, + btc.OP.CHECKSEQUENCEVERIFY, + btc.OP.DROP, + 0x20, + ...userTimeoutPublicKey, + btc.OP.CHECKSIG, + ]); + + const hashScript = new Uint8Array([ + btc.OP.SHA256, + 0x20, + ...hash, + btc.OP.EQUALVERIFY, + 0x20, + ...userPublicKey, + btc.OP.CHECKSIG, + 0x20, + ...attestorsPublicKey, + btc.OP.CHECKSIGADD, + btc.OP.OP_2, + btc.OP.EQUAL, + ]); + + const taprootTree = btc.taprootListToTree([ + { + script: timeoutScript, + leafVersion: 0xc0, + }, + { + script: hashScript, + leafVersion: 0xc0, + }, + ]); + + return taprootTree; + } + + function createAddresses( + userPublicKey: Uint8Array, + attestorPublicKey: Uint8Array, + btcNetwork: BitcoinNetwork, + taprootTree: btc.TaprootScriptTree + ): { + htlcAddress: string; + htlcTransaction: btc.P2TROut; + multisigTransaction: btc.P2TROut; + multisigAddress: string; + } { + const htlcTransaction = btc.p2tr(undefined, taprootTree, btcNetwork, true); + const htlcAddress = htlcTransaction.address; + + const multisig = btc.p2tr_ns(2, [userPublicKey, attestorPublicKey]); + const multisigTransaction = btc.p2tr(undefined, multisig, btcNetwork); + const multisigAddress = multisigTransaction.address; + + if (!htlcAddress) throw new BitcoinError('Could not create HTLC address'); + if (!multisigAddress) throw new BitcoinError('Could not create multisig address'); + + return { htlcAddress, htlcTransaction, multisigTransaction, multisigAddress }; + } + + function createPrefundingTransaction( + utxos: any[], + htlcAddress: string, + userChangeAddress: string, + btcAmount: number, + btcNetwork: BitcoinNetwork + ) { + console.log('prefundingTransaction'); + console.log('utxos', utxos); + console.log('htlcAddress', htlcAddress); + console.log('userChangeAddress', userChangeAddress); + console.log('btcAmount', btcAmount); + console.log('btcNetwork', btcNetwork); + const outputs = [ + { address: htlcAddress, amount: BigInt(btcAmount) }, // amount in satoshi + ]; + + const selected = btc.selectUTXO(utxos, outputs, 'default', { + changeAddress: userChangeAddress, // required, address to send change + feePerByte: 2n, // require, fee per vbyte in satoshi + bip69: false, // ?? // lexicographical Indexing of Transaction Inputs and Outputs + createTx: true, // create tx with selected inputs/outputs + network: btcNetwork, + }); + console.log('selected', selected); + + const prefundingTransaction = selected?.tx; + + return prefundingTransaction?.toPSBT(); + } + + function createFundingTransaction( + prefundingTransaction: any, + htlcTransaction: any, + btcAmount: number, + multisigAddress: string, + btcNetwork: BitcoinNetwork + ): Uint8Array { + const fundingTX = new btc.Transaction(); + const prefundingInput = { + txid: hexToBytes(prefundingTransaction.id), + index: 0, + witnessUtxo: { amount: BigInt(btcAmount), script: htlcTransaction.script }, + ...htlcTransaction, }; - const urlParams = { - bitcoinContractOffer: JSON.stringify(bitcoinContractOffer), - bitcoinNetwork: JSON.stringify('regtest'), - counterpartyWalletDetails: JSON.stringify(counterPartyWalletDetails), + fundingTX.addInput(prefundingInput); + fundingTX.addOutputAddress(multisigAddress, BigInt(btcAmount), btcNetwork); + const fundingPSBT = fundingTX.toPSBT(); + console.log(fundingPSBT); + return fundingPSBT; + } + + function createClosingTransaction( + fundingTransaction: any, + multisigTransaction: any, + userAddress: string, + btcAmount: number, + btcNetwork: BitcoinNetwork + ): Uint8Array { + console.log('closingTransaction'); + const sha256x2 = (...msgs) => sha256(sha256(concatBytes(...msgs))); + const fundingTransactionID = hex.encode(sha256x2(fundingTransaction).reverse()); + + const closingTransaction = new btc.Transaction(); + const fundingInput = { + txid: hexToBytes(fundingTransactionID), + index: 0, + witnessUtxo: { amount: BigInt(btcAmount), script: multisigTransaction.script }, + ...multisigTransaction, }; - return urlParams; + closingTransaction.addInput(fundingInput); + closingTransaction.addOutputAddress(userAddress, BigInt(btcAmount), btcNetwork); + const closingPSBT = closingTransaction.toPSBT(); + return closingPSBT; } - async function sendOfferForSigning(urlParams: any, vaultUUID: string) { - try { - const response = await window.btc.request('acceptBitcoinContractOffer', urlParams); - if (!network) return; - dispatch( - vaultActions.setVaultToFunding({ - vaultUUID, - fundingTX: response.result.txId, - networkID: network.id, - }) - ); - dispatch(mintUnmintActions.setMintStep([2, vaultUUID])); - } catch (error: any) { - throw new BitcoinError(`Could not send contract offer for signing: ${error.error.message}`); - } + async function signPSBT(psbt: Uint8Array, shouldBroadcast: boolean): Promise { + const requestParams: SignPsbtRequestParams = { + hex: bytesToHex(psbt), + signAtIndex: [0], + broadcast: shouldBroadcast, + }; + const result = await window.btc.request('signPsbt', requestParams); + return result.result.hex; } - async function fetchBitcoinContractOfferFromCounterpartyWallet(vault: Vault) { - try { - const response = await fetch(`${routerWalletURL}/offer`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - uuid: vault.uuid, - }), - }); - const responseStream = await response.json(); - if (!response.ok) { - throw new BitcoinError(responseStream.error.message); - } - return responseStream; - } catch (error: any) { - throw new BitcoinError( - `Could not fetch contract offer from counterparty wallet: ${error.message}` - ); - } + async function handlePrefundingTransaction( + userUTXOs: any[], + htlcAddress: string, + userChangeAddress: string, + btcAmount: number, + btcNetwork: BitcoinNetwork + ): Promise { + const prefundingTransactionPSBT = createPrefundingTransaction( + userUTXOs, + htlcAddress, + userChangeAddress, + btcAmount, + btcNetwork + ); + if (!prefundingTransactionPSBT) + throw new BitcoinError('Could not create prefunding transaction'); + const prefundingTransactionHex = await signPSBT(prefundingTransactionPSBT, true); + const prefundingTransactionBytes = hexToBytes(prefundingTransactionHex); + const prefundingTransaction = btc.Transaction.fromPSBT(prefundingTransactionBytes); + prefundingTransaction.finalize(); + return prefundingTransaction; + } + + async function handleFundingTransaction( + prefundingTransaction: any, + htlcTransaction: any, + btcAmount: number, + multisigAddress: string, + btcNetwork: BitcoinNetwork + ): Promise<{ fundingTransaction: Uint8Array; fundingTransactionHex: string }> { + const fundingTransaction = createFundingTransaction( + prefundingTransaction, + htlcTransaction, + btcAmount, + multisigAddress, + btcNetwork + ); + const fundingTransactionHex = await signPSBT(fundingTransaction, false); + return { fundingTransaction, fundingTransactionHex }; } - async function fetchBitcoinContractOfferAndSendToUserWallet(vault: Vault) { - const bitcoinContractOffer = await fetchBitcoinContractOfferFromCounterpartyWallet(vault); - if (!bitcoinContractOffer) return; - const urlParams = createURLParams(bitcoinContractOffer); - await sendOfferForSigning(urlParams, vault.uuid); + async function handleClosingTransaction( + fundingTransaction: any, + multisigTransaction: any, + userAddress: string, + btcAmount: number, + btcNetwork: BitcoinNetwork + ): Promise { + const closingTransaction = createClosingTransaction( + fundingTransaction, + multisigTransaction, + userAddress, + btcAmount, + btcNetwork + ); + const closingTransactionHex = await signPSBT(closingTransaction, false); + return closingTransactionHex; + } + + async function test(): Promise { + const btcNetwork = regtest; + const testBitcoinAmount = 50_000; + + const userAddresses = await getBitcoinAddresses(); + const userNativeSegwitAddress = userAddresses[0] as BitcoinNativeSegwitAddress; + const userTaprootAddress = userAddresses[1] as BitcoinTaprootAddress; + const userPublicKey = userTaprootAddress.tweakedPublicKey; + const userTimeoutPublicKey = hex.decode( + '5e2aaec4656d843798597c5b08876271fa1aae4f0d32a44240d379b75d3aa4fb' + ); + + const attestorPublicKey = 'a27d8d7e1976c7ffaea08ead4aec592da663bcdda75d49ff4bf92dfcb508476e'; + + const preImage = hexToBytes('107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f'); + const preImageHash = hexToBytes( + '6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333' + ); + + const userUTXOs = await gatherUTXOs(userAddresses[0] as BitcoinNativeSegwitAddress); + const taprootTree = createTaprootTree( + userTimeoutPublicKey, + hex.decode(userPublicKey), + hex.decode(attestorPublicKey), + preImageHash + ); + const { htlcAddress, htlcTransaction, multisigTransaction, multisigAddress } = createAddresses( + hex.decode(userPublicKey), + hex.decode(attestorPublicKey), + btcNetwork, + taprootTree + ); + const prefundingTransaction = await handlePrefundingTransaction( + userUTXOs, + htlcAddress, + 'bcrt1qk5q0takwdva20adgw8zf4vy07w9529gpfkrv6v', + testBitcoinAmount, + btcNetwork + ); + + const { fundingTransaction, fundingTransactionHex } = await handleFundingTransaction( + prefundingTransaction, + htlcTransaction, + testBitcoinAmount, + multisigAddress, + btcNetwork + ); + console.log('fundingTransactionHex', fundingTransactionHex); + + const closingTransactionHex = await handleClosingTransaction( + fundingTransaction, + multisigTransaction, + userNativeSegwitAddress.address, + testBitcoinAmount, + btcNetwork + ); + console.log('closingTransactionHex', closingTransactionHex); } async function fetchBitcoinPrice() { @@ -105,7 +419,7 @@ export function useBitcoin(): UseBitcoinReturnType { } return { - fetchBitcoinContractOfferAndSendToUserWallet, + test, bitcoinPrice, }; } diff --git a/src/shared/models/bitcoin-network.ts b/src/shared/models/bitcoin-network.ts new file mode 100644 index 00000000..8fbb54ed --- /dev/null +++ b/src/shared/models/bitcoin-network.ts @@ -0,0 +1,55 @@ +interface Bip32 { + public: number; + private: number; +} + +export interface BitcoinNetwork { + messagePrefix: string; + bech32: string; + bip32: Bip32; + pubKeyHash: number; + scriptHash: number; + wif: number; + bytes: number; + 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 regtest: BitcoinNetwork = { + messagePrefix: '\x18Bitcoin Signed Message:\n', + bech32: 'bcrt', + 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, +}; From 37279cc36b6b464dbbb225e43327f510468ed3fe Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 31 Jan 2024 11:32:29 +0100 Subject: [PATCH 02/14] feat: modify flow by removing prefunding transaction --- .../transaction-form/transaction-form.tsx | 12 +- .../components/walkthrough/walkthrough.tsx | 8 +- src/app/hooks/use-bitcoin.ts | 205 ++++-------------- 3 files changed, 52 insertions(+), 173 deletions(-) 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 33f2bc95..c47f543f 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 @@ -7,6 +7,7 @@ import { Form, Formik } from 'formik'; import { TransactionFormInput } from './components/transaction-form-input'; import { TransactionFormWarning } from './components/transaction-form-warning'; +import { customShiftValue } from '@common/utilities'; export interface TransactionFormValues { amount: number; @@ -20,12 +21,11 @@ export function TransactionForm(): React.JSX.Element { const bitcoinPrice = blockchainContext?.bitcoin.bitcoinPrice; const [isSubmitting, setIsSubmitting] = useState(false); - async function handleSetup() { + async function handleSetup(btcDepositAmount: number) { try { setIsSubmitting(true); - // const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); - // await ethereum?.setupVault(shiftedBTCDepositAmount); - blockchainContext?.bitcoin.test(); + const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); + await blockchainContext?.bitcoin.lockBitcoin(shiftedBTCDepositAmount); } catch (error) { setIsSubmitting(false); toast({ @@ -43,7 +43,7 @@ export function TransactionForm(): React.JSX.Element { { - await handleSetup(); + await handleSetup(values.amount); }} > {({ handleSubmit, errors, touched, values }) => ( @@ -62,7 +62,7 @@ export function TransactionForm(): React.JSX.Element { type={'submit'} isDisabled={Boolean(errors.amount)} > - Create Vault + Lock Bitcoin diff --git a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx index 7ab2a4d0..8c6c6a47 100644 --- a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx +++ b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx @@ -25,17 +25,17 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. - Select an amount of dlcBTC you would like to mint and confirm it in your{' '} + Select an amount of dlcBTC you would like to mint and sign the required transactions in your{' '} - Ethereum Wallet + Bitcoin Wallet . diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index 4b5c2a82..153fadad 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -65,11 +65,6 @@ interface StacksAddress { symbol: string; } -interface BitcoinAddresses { - nativeSegwit: BitcoinNativeSegwitAddress; - taproot: BitcoinTaprootAddress; -} - type Address = BitcoinNativeSegwitAddress | BitcoinTaprootAddress | StacksAddress; interface RpcResult { addresses: Address[]; @@ -82,7 +77,7 @@ interface RpcResponse { } export interface UseBitcoinReturnType { - test: () => Promise; + lockBitcoin: (btcAmount: number) => Promise; bitcoinPrice: number; } @@ -99,19 +94,18 @@ export function useBitcoin(): UseBitcoinReturnType { }, []); async function getBitcoinAddresses(): Promise { - console.log('getBitcoinAddresses'); const rpcResponse: RpcResponse = await window.btc?.request('getAddresses'); const userAddresses = rpcResponse.result.addresses; return userAddresses; } async function gatherUTXOs(bitcoinNativeSegwitAddress: BitcoinNativeSegwitAddress): Promise { - console.log('gatherUTXOs'); const response = await fetch( `${ELECTRUM_API_URL}/address/${bitcoinNativeSegwitAddress.address}/utxo` ); const allUTXOs = await response.json(); - const spend = btc.p2wpkh(bitcoinNativeSegwitAddress.publicKey, regtest); + const userPublicKey = hexToBytes(bitcoinNativeSegwitAddress.publicKey); + const spend = btc.p2wpkh(userPublicKey, regtest); const utxos = await Promise.all( allUTXOs.map(async (utxo: UTXO) => { @@ -130,92 +124,32 @@ export function useBitcoin(): UseBitcoinReturnType { return utxos; } - function createTaprootTree( - userTimeoutPublicKey: Uint8Array, - userPublicKey: Uint8Array, - attestorsPublicKey: Uint8Array, - hash: Uint8Array - ): btc.TaprootScriptTree { - console.log('createTaprootTree'); - const timeoutScript = new Uint8Array([ - 0x02, - 144, - 0x00, - btc.OP.CHECKSEQUENCEVERIFY, - btc.OP.DROP, - 0x20, - ...userTimeoutPublicKey, - btc.OP.CHECKSIG, - ]); - - const hashScript = new Uint8Array([ - btc.OP.SHA256, - 0x20, - ...hash, - btc.OP.EQUALVERIFY, - 0x20, - ...userPublicKey, - btc.OP.CHECKSIG, - 0x20, - ...attestorsPublicKey, - btc.OP.CHECKSIGADD, - btc.OP.OP_2, - btc.OP.EQUAL, - ]); - - const taprootTree = btc.taprootListToTree([ - { - script: timeoutScript, - leafVersion: 0xc0, - }, - { - script: hashScript, - leafVersion: 0xc0, - }, - ]); - - return taprootTree; - } - - function createAddresses( + function createMultisigTransactionAndAddress( userPublicKey: Uint8Array, attestorPublicKey: Uint8Array, btcNetwork: BitcoinNetwork, - taprootTree: btc.TaprootScriptTree ): { - htlcAddress: string; - htlcTransaction: btc.P2TROut; multisigTransaction: btc.P2TROut; multisigAddress: string; } { - const htlcTransaction = btc.p2tr(undefined, taprootTree, btcNetwork, true); - const htlcAddress = htlcTransaction.address; - const multisig = btc.p2tr_ns(2, [userPublicKey, attestorPublicKey]); const multisigTransaction = btc.p2tr(undefined, multisig, btcNetwork); const multisigAddress = multisigTransaction.address; - if (!htlcAddress) throw new BitcoinError('Could not create HTLC address'); if (!multisigAddress) throw new BitcoinError('Could not create multisig address'); - return { htlcAddress, htlcTransaction, multisigTransaction, multisigAddress }; + return { multisigTransaction, multisigAddress }; } - function createPrefundingTransaction( - utxos: any[], - htlcAddress: string, + function createFundingTransaction( + multisigAddress: string, userChangeAddress: string, + utxos: any[], btcAmount: number, btcNetwork: BitcoinNetwork - ) { - console.log('prefundingTransaction'); - console.log('utxos', utxos); - console.log('htlcAddress', htlcAddress); - console.log('userChangeAddress', userChangeAddress); - console.log('btcAmount', btcAmount); - console.log('btcNetwork', btcNetwork); + ): Uint8Array { const outputs = [ - { address: htlcAddress, amount: BigInt(btcAmount) }, // amount in satoshi + { address: multisigAddress, amount: BigInt(btcAmount) }, // amount in satoshi ]; const selected = btc.selectUTXO(utxos, outputs, 'default', { @@ -225,45 +159,28 @@ export function useBitcoin(): UseBitcoinReturnType { createTx: true, // create tx with selected inputs/outputs network: btcNetwork, }); - console.log('selected', selected); - const prefundingTransaction = selected?.tx; + const fundingTX = selected?.tx; - return prefundingTransaction?.toPSBT(); - } + if (!fundingTX) throw new BitcoinError('Could not create funding transaction'); - function createFundingTransaction( - prefundingTransaction: any, - htlcTransaction: any, - btcAmount: number, - multisigAddress: string, - btcNetwork: BitcoinNetwork - ): Uint8Array { - const fundingTX = new btc.Transaction(); - const prefundingInput = { - txid: hexToBytes(prefundingTransaction.id), - index: 0, - witnessUtxo: { amount: BigInt(btcAmount), script: htlcTransaction.script }, - ...htlcTransaction, - }; - fundingTX.addInput(prefundingInput); - fundingTX.addOutputAddress(multisigAddress, BigInt(btcAmount), btcNetwork); const fundingPSBT = fundingTX.toPSBT(); - console.log(fundingPSBT); 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 createClosingTransaction( - fundingTransaction: any, + fundingTransactionID: string, multisigTransaction: any, - userAddress: string, + userNativeSegwitAddress: string, btcAmount: number, btcNetwork: BitcoinNetwork ): Uint8Array { - console.log('closingTransaction'); - const sha256x2 = (...msgs) => sha256(sha256(concatBytes(...msgs))); - const fundingTransactionID = hex.encode(sha256x2(fundingTransaction).reverse()); - const closingTransaction = new btc.Transaction(); const fundingInput = { txid: hexToBytes(fundingTransactionID), @@ -272,7 +189,7 @@ export function useBitcoin(): UseBitcoinReturnType { ...multisigTransaction, }; closingTransaction.addInput(fundingInput); - closingTransaction.addOutputAddress(userAddress, BigInt(btcAmount), btcNetwork); + closingTransaction.addOutputAddress(userNativeSegwitAddress, BigInt(btcAmount), btcNetwork); const closingPSBT = closingTransaction.toPSBT(); return closingPSBT; } @@ -287,44 +204,22 @@ export function useBitcoin(): UseBitcoinReturnType { return result.result.hex; } - async function handlePrefundingTransaction( - userUTXOs: any[], - htlcAddress: string, - userChangeAddress: string, - btcAmount: number, - btcNetwork: BitcoinNetwork - ): Promise { - const prefundingTransactionPSBT = createPrefundingTransaction( - userUTXOs, - htlcAddress, - userChangeAddress, - btcAmount, - btcNetwork - ); - if (!prefundingTransactionPSBT) - throw new BitcoinError('Could not create prefunding transaction'); - const prefundingTransactionHex = await signPSBT(prefundingTransactionPSBT, true); - const prefundingTransactionBytes = hexToBytes(prefundingTransactionHex); - const prefundingTransaction = btc.Transaction.fromPSBT(prefundingTransactionBytes); - prefundingTransaction.finalize(); - return prefundingTransaction; - } async function handleFundingTransaction( - prefundingTransaction: any, - htlcTransaction: any, +multisigAddress: string, +userChangeAddress: string, +utxos: any[], btcAmount: number, - multisigAddress: string, btcNetwork: BitcoinNetwork ): Promise<{ fundingTransaction: Uint8Array; fundingTransactionHex: string }> { const fundingTransaction = createFundingTransaction( - prefundingTransaction, - htlcTransaction, - btcAmount, multisigAddress, + userChangeAddress, + utxos, + btcAmount, btcNetwork ); - const fundingTransactionHex = await signPSBT(fundingTransaction, false); + const fundingTransactionHex = await signPSBT(fundingTransaction, true); return { fundingTransaction, fundingTransactionHex }; } @@ -346,60 +241,44 @@ export function useBitcoin(): UseBitcoinReturnType { return closingTransactionHex; } - async function test(): Promise { + async function lockBitcoin(btcAmount: number): Promise { const btcNetwork = regtest; - const testBitcoinAmount = 50_000; const userAddresses = await getBitcoinAddresses(); const userNativeSegwitAddress = userAddresses[0] as BitcoinNativeSegwitAddress; const userTaprootAddress = userAddresses[1] as BitcoinTaprootAddress; const userPublicKey = userTaprootAddress.tweakedPublicKey; - const userTimeoutPublicKey = hex.decode( - '5e2aaec4656d843798597c5b08876271fa1aae4f0d32a44240d379b75d3aa4fb' - ); const attestorPublicKey = 'a27d8d7e1976c7ffaea08ead4aec592da663bcdda75d49ff4bf92dfcb508476e'; - const preImage = hexToBytes('107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f'); - const preImageHash = hexToBytes( - '6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333' - ); + // const preImage = hexToBytes('107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f'); + // const preImageHash = hexToBytes( + // '6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333' + // ); const userUTXOs = await gatherUTXOs(userAddresses[0] as BitcoinNativeSegwitAddress); - const taprootTree = createTaprootTree( - userTimeoutPublicKey, - hex.decode(userPublicKey), - hex.decode(attestorPublicKey), - preImageHash - ); - const { htlcAddress, htlcTransaction, multisigTransaction, multisigAddress } = createAddresses( + const { multisigTransaction, multisigAddress } = createMultisigTransactionAndAddress( hex.decode(userPublicKey), hex.decode(attestorPublicKey), btcNetwork, - taprootTree - ); - const prefundingTransaction = await handlePrefundingTransaction( - userUTXOs, - htlcAddress, - 'bcrt1qk5q0takwdva20adgw8zf4vy07w9529gpfkrv6v', - testBitcoinAmount, - btcNetwork ); const { fundingTransaction, fundingTransactionHex } = await handleFundingTransaction( - prefundingTransaction, - htlcTransaction, - testBitcoinAmount, multisigAddress, + userNativeSegwitAddress.address, + userUTXOs, + btcAmount, btcNetwork ); console.log('fundingTransactionHex', fundingTransactionHex); + const fundingTransactionID = getFundingTransactionID(fundingTransaction); + const closingTransactionHex = await handleClosingTransaction( - fundingTransaction, + fundingTransactionID, multisigTransaction, userNativeSegwitAddress.address, - testBitcoinAmount, + btcAmount, btcNetwork ); console.log('closingTransactionHex', closingTransactionHex); @@ -419,7 +298,7 @@ export function useBitcoin(): UseBitcoinReturnType { } return { - test, + lockBitcoin, bitcoinPrice, }; } From 51e5b5d0830347ed527007b95eeb2f68edbedac6 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 31 Jan 2024 12:26:44 +0100 Subject: [PATCH 03/14] feat: add ui to transaction form to show psbt flow --- .../transaction-form/transaction-form.tsx | 44 +++++++++++++++++-- src/app/hooks/use-bitcoin.ts | 37 +++++++++++----- 2 files changed, 67 insertions(+), 14 deletions(-) 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 c47f543f..472dfcb6 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,6 +1,6 @@ import { useContext, useState } from 'react'; -import { Button, FormControl, FormErrorMessage, Text, VStack, useToast } from '@chakra-ui/react'; +import { Button, Checkbox, Fade, FormControl, FormErrorMessage, HStack, Stack, Text, VStack, useToast } from '@chakra-ui/react'; import { EthereumError } from '@models/error-types'; import { BlockchainContext } from '@providers/blockchain-context-provider'; import { Form, Formik } from 'formik'; @@ -8,6 +8,7 @@ import { Form, Formik } from 'formik'; import { TransactionFormInput } from './components/transaction-form-input'; import { TransactionFormWarning } from './components/transaction-form-warning'; import { customShiftValue } from '@common/utilities'; +import { SignAndBroadcastFundingPSBTResult } from '@hooks/use-bitcoin'; export interface TransactionFormValues { amount: number; @@ -19,13 +20,32 @@ export function TransactionForm(): React.JSX.Element { const toast = useToast(); const blockchainContext = useContext(BlockchainContext); const bitcoinPrice = blockchainContext?.bitcoin.bitcoinPrice; + const [fundingTransactionSigned, setFundingTransactionSigned] = useState(false); + const [closingTransactionSigned, setClosingTransactionSigned] = useState(false); + const [fundingTransactionResult, setFundingTransactionResult] = useState(); const [isSubmitting, setIsSubmitting] = useState(false); async function handleSetup(btcDepositAmount: number) { try { setIsSubmitting(true); const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); - await blockchainContext?.bitcoin.lockBitcoin(shiftedBTCDepositAmount); + if (!fundingTransactionSigned) { + const fundingResult = await blockchainContext?.bitcoin.signAndBroadcastFundingPSBT(shiftedBTCDepositAmount); + if (!fundingResult) { + throw new Error('Failed to create vault'); + } + setFundingTransactionSigned(true); + setFundingTransactionResult(fundingResult); + await blockchainContext?.bitcoin.signClosingPSBT(fundingResult); + setClosingTransactionSigned(true) + } else { + if (!fundingTransactionResult) { + throw new Error('Failed to create vault'); + } + await blockchainContext?.bitcoin.signClosingPSBT(fundingTransactionResult); + setClosingTransactionSigned(true) + } + setIsSubmitting(false); } catch (error) { setIsSubmitting(false); toast({ @@ -62,8 +82,26 @@ export function TransactionForm(): React.JSX.Element { type={'submit'} isDisabled={Boolean(errors.amount)} > - Lock Bitcoin + {fundingTransactionSigned ? 'Sign Closing Transaction' : 'Lock Bitcoin'} + + + + + + + Funding Transaction Signed + + + + + + Closing Transaction Signed + + + + + diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index 153fadad..361c1fe9 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -77,14 +77,23 @@ interface RpcResponse { } export interface UseBitcoinReturnType { - lockBitcoin: (btcAmount: number) => Promise; + signAndBroadcastFundingPSBT: (btcAmount: number) => Promise; + signClosingPSBT: (params: SignAndBroadcastFundingPSBTResult) => Promise; bitcoinPrice: number; } +export interface SignAndBroadcastFundingPSBTResult { + fundingTransactionID: string, + multisigTransaction: btc.P2TROut, + userNativeSegwitAddress: string, + btcAmount: number +} + const ELECTRUM_API_URL = 'https://devnet.dlc.link/electrs'; export function useBitcoin(): UseBitcoinReturnType { const [bitcoinPrice, setBitcoinPrice] = useState(0); + const [btcNetwork, setBTCNetwork] = useState(regtest); useEffect(() => { const getBitcoinPrice = async () => { @@ -224,14 +233,14 @@ utxos: any[], } async function handleClosingTransaction( - fundingTransaction: any, - multisigTransaction: any, + fundingTransactionID: string, + multisigTransaction: btc.P2TROut, userAddress: string, btcAmount: number, btcNetwork: BitcoinNetwork ): Promise { const closingTransaction = createClosingTransaction( - fundingTransaction, + fundingTransactionID, multisigTransaction, userAddress, btcAmount, @@ -241,11 +250,10 @@ utxos: any[], return closingTransactionHex; } - async function lockBitcoin(btcAmount: number): Promise { - const btcNetwork = regtest; - + async function signAndBroadcastFundingPSBT(btcAmount: number): Promise { const userAddresses = await getBitcoinAddresses(); - const userNativeSegwitAddress = userAddresses[0] as BitcoinNativeSegwitAddress; + const userNativeSegwitAccount = userAddresses[0] as BitcoinNativeSegwitAddress; + const userNativeSegwitAddress = userNativeSegwitAccount.address; const userTaprootAddress = userAddresses[1] as BitcoinTaprootAddress; const userPublicKey = userTaprootAddress.tweakedPublicKey; @@ -263,9 +271,10 @@ utxos: any[], btcNetwork, ); + const { fundingTransaction, fundingTransactionHex } = await handleFundingTransaction( multisigAddress, - userNativeSegwitAddress.address, + userNativeSegwitAddress, userUTXOs, btcAmount, btcNetwork @@ -274,10 +283,15 @@ utxos: any[], const fundingTransactionID = getFundingTransactionID(fundingTransaction); + return { fundingTransactionID, multisigTransaction, userNativeSegwitAddress, btcAmount }; + } + + async function signClosingPSBT({fundingTransactionID, multisigTransaction, userNativeSegwitAddress, btcAmount} : SignAndBroadcastFundingPSBTResult): Promise { + if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) console.error('missing data'); const closingTransactionHex = await handleClosingTransaction( fundingTransactionID, multisigTransaction, - userNativeSegwitAddress.address, + userNativeSegwitAddress, btcAmount, btcNetwork ); @@ -298,7 +312,8 @@ utxos: any[], } return { - lockBitcoin, + signAndBroadcastFundingPSBT, + signClosingPSBT, bitcoinPrice, }; } From 96769c7d48748081212800932323598a50aa514d Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 31 Jan 2024 15:30:33 +0100 Subject: [PATCH 04/14] feat: add signpsbt hook to extract logic from the the form --- .../transaction-form/transaction-form.tsx | 96 +++++++++++-------- .../components/walkthrough/walkthrough.tsx | 3 +- src/app/hooks/use-bitcoin.ts | 65 +++++++++---- src/app/hooks/use-sign-psbt.ts | 86 +++++++++++++++++ 4 files changed, 188 insertions(+), 62 deletions(-) create mode 100644 src/app/hooks/use-sign-psbt.ts 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 472dfcb6..e06aa06b 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,14 +1,25 @@ import { useContext, useState } from 'react'; -import { Button, Checkbox, Fade, FormControl, FormErrorMessage, HStack, Stack, Text, VStack, useToast } from '@chakra-ui/react'; -import { EthereumError } from '@models/error-types'; +import { + Button, + Checkbox, + Fade, + FormControl, + FormErrorMessage, + HStack, + Stack, + 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 { BlockchainContext } from '@providers/blockchain-context-provider'; import { Form, Formik } from 'formik'; import { TransactionFormInput } from './components/transaction-form-input'; import { TransactionFormWarning } from './components/transaction-form-warning'; -import { customShiftValue } from '@common/utilities'; -import { SignAndBroadcastFundingPSBTResult } from '@hooks/use-bitcoin'; export interface TransactionFormValues { amount: number; @@ -19,38 +30,23 @@ 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 bitcoinPrice = blockchainContext?.bitcoin.bitcoinPrice; - const [fundingTransactionSigned, setFundingTransactionSigned] = useState(false); - const [closingTransactionSigned, setClosingTransactionSigned] = useState(false); - const [fundingTransactionResult, setFundingTransactionResult] = useState(); const [isSubmitting, setIsSubmitting] = useState(false); async function handleSetup(btcDepositAmount: number) { try { setIsSubmitting(true); const shiftedBTCDepositAmount = customShiftValue(btcDepositAmount, 8, false); - if (!fundingTransactionSigned) { - const fundingResult = await blockchainContext?.bitcoin.signAndBroadcastFundingPSBT(shiftedBTCDepositAmount); - if (!fundingResult) { - throw new Error('Failed to create vault'); - } - setFundingTransactionSigned(true); - setFundingTransactionResult(fundingResult); - await blockchainContext?.bitcoin.signClosingPSBT(fundingResult); - setClosingTransactionSigned(true) - } else { - if (!fundingTransactionResult) { - throw new Error('Failed to create vault'); - } - await blockchainContext?.bitcoin.signClosingPSBT(fundingTransactionResult); - setClosingTransactionSigned(true) - } + await handleSignTransaction(shiftedBTCDepositAmount); setIsSubmitting(false); } catch (error) { setIsSubmitting(false); toast({ - title: 'Failed to create vault', - description: error instanceof EthereumError ? error.message : '', + title: 'Failed to sign transaction', + description: error instanceof BitcoinError ? error.message : '', status: 'error', duration: 9000, isClosable: true, @@ -84,23 +80,39 @@ export function TransactionForm(): React.JSX.Element { > {fundingTransactionSigned ? 'Sign Closing Transaction' : 'Lock Bitcoin'} - - - - - - - Funding Transaction Signed - - - - - - Closing Transaction Signed - - - - + + + + + + + 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 8c6c6a47..e52befff 100644 --- a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx +++ b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx @@ -28,7 +28,8 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. blockchain={'bitcoin'} /> - 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 sign the required transactions + in your{' '} Promise; - signClosingPSBT: (params: SignAndBroadcastFundingPSBTResult) => Promise; + signAndBroadcastFundingPSBT: ( + btcAmount: number + ) => Promise<{ + fundingTransactionID: string; + multisigTransaction: btc.P2TROut; + userNativeSegwitAddress: string; + btcAmount: number; + }>; + signClosingPSBT: ( + fundingTransactionID: string, + multisigTransaction: btc.P2TROut, + userNativeSegwitAddress: string, + btcAmount: number + ) => Promise; bitcoinPrice: number; } export interface SignAndBroadcastFundingPSBTResult { - fundingTransactionID: string, - multisigTransaction: btc.P2TROut, - userNativeSegwitAddress: string, - btcAmount: number + fundingTransactionID: string; + multisigTransaction: btc.P2TROut; + userNativeSegwitAddress: string; + btcAmount: number; } const ELECTRUM_API_URL = 'https://devnet.dlc.link/electrs'; @@ -103,9 +115,13 @@ export function useBitcoin(): UseBitcoinReturnType { }, []); async function getBitcoinAddresses(): Promise { - const rpcResponse: RpcResponse = await window.btc?.request('getAddresses'); - const userAddresses = rpcResponse.result.addresses; - return userAddresses; + try { + const rpcResponse: RpcResponse = await window.btc?.request('getAddresses'); + const userAddresses = rpcResponse.result.addresses; + return userAddresses; + } catch (error) { + throw new BitcoinError(`Error getting bitcoin addresses: ${error}`); + } } async function gatherUTXOs(bitcoinNativeSegwitAddress: BitcoinNativeSegwitAddress): Promise { @@ -136,7 +152,7 @@ export function useBitcoin(): UseBitcoinReturnType { function createMultisigTransactionAndAddress( userPublicKey: Uint8Array, attestorPublicKey: Uint8Array, - btcNetwork: BitcoinNetwork, + btcNetwork: BitcoinNetwork ): { multisigTransaction: btc.P2TROut; multisigAddress: string; @@ -213,11 +229,10 @@ export function useBitcoin(): UseBitcoinReturnType { return result.result.hex; } - async function handleFundingTransaction( -multisigAddress: string, -userChangeAddress: string, -utxos: any[], + multisigAddress: string, + userChangeAddress: string, + utxos: any[], btcAmount: number, btcNetwork: BitcoinNetwork ): Promise<{ fundingTransaction: Uint8Array; fundingTransactionHex: string }> { @@ -250,7 +265,14 @@ utxos: any[], return closingTransactionHex; } - async function signAndBroadcastFundingPSBT(btcAmount: number): Promise { + async function signAndBroadcastFundingPSBT( + btcAmount: number + ): Promise<{ + fundingTransactionID: string; + multisigTransaction: btc.P2TROut; + userNativeSegwitAddress: string; + btcAmount: number; + }> { const userAddresses = await getBitcoinAddresses(); const userNativeSegwitAccount = userAddresses[0] as BitcoinNativeSegwitAddress; const userNativeSegwitAddress = userNativeSegwitAccount.address; @@ -268,10 +290,9 @@ utxos: any[], const { multisigTransaction, multisigAddress } = createMultisigTransactionAndAddress( hex.decode(userPublicKey), hex.decode(attestorPublicKey), - btcNetwork, + btcNetwork ); - const { fundingTransaction, fundingTransactionHex } = await handleFundingTransaction( multisigAddress, userNativeSegwitAddress, @@ -286,8 +307,14 @@ utxos: any[], return { fundingTransactionID, multisigTransaction, userNativeSegwitAddress, btcAmount }; } - async function signClosingPSBT({fundingTransactionID, multisigTransaction, userNativeSegwitAddress, btcAmount} : SignAndBroadcastFundingPSBTResult): Promise { - if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) console.error('missing data'); + async function signClosingPSBT( + fundingTransactionID: string, + multisigTransaction: btc.P2TROut, + userNativeSegwitAddress: string, + btcAmount: number + ): Promise { + if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) + console.error('missing data'); const closingTransactionHex = await handleClosingTransaction( fundingTransactionID, multisigTransaction, diff --git a/src/app/hooks/use-sign-psbt.ts b/src/app/hooks/use-sign-psbt.ts new file mode 100644 index 00000000..3c1bf1da --- /dev/null +++ b/src/app/hooks/use-sign-psbt.ts @@ -0,0 +1,86 @@ +import { useEffect, useState } from 'react'; + +import { BitcoinError } from '@models/error-types'; +import * as btc from '@scure/btc-signer'; + +import { UseBitcoinReturnType } from './use-bitcoin'; + +export interface UseSignPSBTReturnType { + handleSignTransaction: (btcAmount: number) => Promise; + fundingTransactionSigned: boolean; + closingTransactionSigned: boolean; +} + +export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTReturnType { + if (!useBitcoin) throw new Error('useBitcoin must be set before useSignPSBT can be used'); + + const { signAndBroadcastFundingPSBT, signClosingPSBT } = useBitcoin; + const [btcAmount, setBTCAmount] = useState(); + const [fundingTransactionSigned, setFundingTransactionSigned] = useState(false); + const [closingTransactionSigned, setClosingTransactionSigned] = useState(false); + const [fundingTransactionID, setFundingTransactionID] = useState(); + const [multisigTransaction, setMultisigTransaction] = useState(); + const [userNativeSegwitAddress, setUserNativeSegwitAddress] = useState(); + + useEffect(() => { + if ( + fundingTransactionSigned && + btcAmount && + fundingTransactionID && + multisigTransaction && + userNativeSegwitAddress + ) { + handleSignClosingTransaction(); + } + }, [ + fundingTransactionSigned, + btcAmount, + fundingTransactionID, + multisigTransaction, + userNativeSegwitAddress, + ]); + + async function handleSignFundingTransaction(btcAmount: number) { + try { + const { fundingTransactionID, multisigTransaction, userNativeSegwitAddress } = + await signAndBroadcastFundingPSBT(btcAmount); + setBTCAmount(btcAmount); + setFundingTransactionID(fundingTransactionID); + setMultisigTransaction(multisigTransaction); + setUserNativeSegwitAddress(userNativeSegwitAddress); + setFundingTransactionSigned(true); + } catch (error) { + throw new BitcoinError(`Error signing funding transaction: ${error}`); + } + } + + async function handleSignClosingTransaction() { + try { + if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) { + throw new Error( + 'Funding transaction must be signed before closing transaction can be signed' + ); + } + await signClosingPSBT( + fundingTransactionID, + multisigTransaction, + userNativeSegwitAddress, + btcAmount + ); + setClosingTransactionSigned(true); + } catch (error) { + throw new BitcoinError(`Error signing closing transaction: ${error}`); + } + } + + async function handleSignTransaction(btcAmount: number) { + setBTCAmount(btcAmount); + if (!fundingTransactionSigned) { + await handleSignFundingTransaction(btcAmount); + } else { + await handleSignClosingTransaction(); + } + } + + return { handleSignTransaction, fundingTransactionSigned, closingTransactionSigned }; +} From 95df24c74db1038ac1941f5ac6644d8985bfb44a Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 5 Feb 2024 10:05:58 +0100 Subject: [PATCH 05/14] 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 | 108 ++++++++++++------ src/app/hooks/use-sign-psbt.ts | 75 +++++++++--- src/shared/models/bitcoin-network.ts | 53 ++++----- 6 files changed, 211 insertions(+), 141 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..bdd8618d 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,40 @@ 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' }, + body: JSON.stringify({ + uuid, + closingPsbt: closingPSBT, + mintAddress: userNativeSegwitAddress, + }), + }); + console.log('response', response); + } catch (error) { + throw new BitcoinError(`Error sending PSBT: ${error}`); + } + } + async function createClosingTransaction( fundingTransactionID: string, multisigTransaction: any, userNativeSegwitAddress: string, btcAmount: number, btcNetwork: BitcoinNetwork - ): Uint8Array { + ): Promise { const closingTransaction = new btc.Transaction(); const fundingInput = { txid: hexToBytes(fundingTransactionID), @@ -235,7 +260,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,18 +268,29 @@ 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( + const closingTransaction = await createClosingTransaction( fundingTransactionID, multisigTransaction, userAddress, @@ -262,12 +298,13 @@ export function useBitcoin(): UseBitcoinReturnType { btcNetwork ); const closingTransactionHex = await signPSBT(closingTransaction, false); + + const response = await sendPSBT(closingTransactionHex, uuid, userAddress); + console.log('response', response); return closingTransactionHex; } - async function signAndBroadcastFundingPSBT( - btcAmount: number - ): Promise<{ + async function signAndBroadcastFundingPSBT(btcAmount: number): Promise<{ fundingTransactionID: string; multisigTransaction: btc.P2TROut; userNativeSegwitAddress: string; @@ -279,7 +316,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 +330,7 @@ export function useBitcoin(): UseBitcoinReturnType { btcNetwork ); - const { fundingTransaction, fundingTransactionHex } = await handleFundingTransaction( + const { fundingTransactionHex, fundingTransactionID } = await handleFundingTransaction( multisigAddress, userNativeSegwitAddress, userUTXOs, @@ -302,23 +339,24 @@ 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 { - if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) - console.error('missing data'); + if (!fundingTransactionID || !multisigTransaction || !userNativeSegwitAddress || !btcAmount) { + throw new BitcoinError('Missing parameters to sign closing PSBT'); + } const closingTransactionHex = await handleClosingTransaction( 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, +// }; From 22c651251193c338ab5853d805baaa8360771e9e Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Mon, 5 Feb 2024 15:34:29 +0100 Subject: [PATCH 06/14] feat: add working attestor communication --- src/app/hooks/use-bitcoin.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index bdd8618d..fb00277e 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -149,7 +149,6 @@ export function useBitcoin(): UseBitcoinReturnType { async function getAttestorPublicKey(): Promise { const response = await fetch('http://localhost:3000/publickey'); const attestorPublicKey = await response.text(); - console.log('attestorPublicKey', attestorPublicKey); return attestorPublicKey; } @@ -212,7 +211,7 @@ export function useBitcoin(): UseBitcoinReturnType { try { const response = await fetch('http://localhost:3000/create-psbt-event', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ uuid, closingPsbt: closingPSBT, @@ -231,7 +230,8 @@ export function useBitcoin(): UseBitcoinReturnType { btcAmount: number, btcNetwork: BitcoinNetwork ): Promise { - const closingTransaction = new btc.Transaction(); + const closingTransaction = new btc.Transaction({ PSBTVersion: 0 }); + const fundingInput = { txid: hexToBytes(fundingTransactionID), index: 0, @@ -239,7 +239,7 @@ export function useBitcoin(): UseBitcoinReturnType { ...multisigTransaction, }; closingTransaction.addInput(fundingInput); - closingTransaction.addOutputAddress(userNativeSegwitAddress, BigInt(btcAmount), btcNetwork); + closingTransaction.addOutputAddress(userNativeSegwitAddress, BigInt(btcAmount - 10000), btcNetwork); const closingPSBT = closingTransaction.toPSBT(); return closingPSBT; } @@ -299,8 +299,7 @@ export function useBitcoin(): UseBitcoinReturnType { ); const closingTransactionHex = await signPSBT(closingTransaction, false); - const response = await sendPSBT(closingTransactionHex, uuid, userAddress); - console.log('response', response); + await sendPSBT(closingTransactionHex, uuid, userAddress); return closingTransactionHex; } From b505504a42152c649056ef272c129c3222ab85b2 Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Wed, 7 Feb 2024 10:22:47 +0100 Subject: [PATCH 07/14] feat: add chainname to attestor call --- src/app/hooks/use-bitcoin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index fb00277e..40723c69 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -216,6 +216,7 @@ export function useBitcoin(): UseBitcoinReturnType { uuid, closingPsbt: closingPSBT, mintAddress: userNativeSegwitAddress, + chain: 'evm-sepolia' }), }); console.log('response', response); From f659e91d21053dfb05f01f5b6ef2ef55517eaf7d Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Thu, 29 Feb 2024 15:05:09 +0100 Subject: [PATCH 08/14] feat: add psbt signing, splitting locking into two steps --- .../components/lock-screen/lock-screen.tsx | 63 ++++----- .../sign-closing-transaction-screen.tsx | 94 +++++++++++++ .../mint-unmint/components/mint/mint.tsx | 25 +++- .../components/progress-timeline-step.tsx | 32 +++-- .../progress-timeline/progress-timeline.tsx | 27 +++- .../components/walkthrough/walkthrough.tsx | 22 +++ src/app/components/vault/vault-card.tsx | 5 +- src/app/hooks/use-bitcoin.ts | 61 ++++----- src/app/hooks/use-blockchain-context.ts | 15 ++ src/app/hooks/use-confirmation-checker.ts | 16 +-- src/app/hooks/use-endpoints.ts | 45 +++--- src/app/hooks/use-psbt.ts | 92 +++++++++++++ src/app/hooks/use-sign-psbt.ts | 129 ------------------ .../providers/blockchain-context-provider.tsx | 2 +- 14 files changed, 376 insertions(+), 252 deletions(-) create mode 100644 src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx create mode 100644 src/app/hooks/use-blockchain-context.ts create mode 100644 src/app/hooks/use-psbt.ts delete mode 100644 src/app/hooks/use-sign-psbt.ts 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 55b87e14..29c23a8f 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,30 +1,39 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { Button, Checkbox, Fade, HStack, Stack, Text, VStack, useToast } from '@chakra-ui/react'; +import { Button, VStack, useToast } from '@chakra-ui/react'; import { VaultCard } from '@components/vault/vault-card'; -import { useSignPSBT } from '@hooks/use-sign-psbt'; +import { UseBitcoinReturnType } from '@hooks/use-bitcoin'; +import { UseEthereumReturnType } from '@hooks/use-ethereum'; +import { UseSignPSBTReturnType } from '@hooks/use-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'; import { LockScreenProtocolFee } from './components/protocol-fee'; interface LockScreenProps { + bitcoinHandler: UseBitcoinReturnType; + ethereumHandler: UseEthereumReturnType; + psbtHandler: UseSignPSBTReturnType; currentStep: [number, string]; } -export function LockScreen({ currentStep }: LockScreenProps): React.JSX.Element { +export function LockScreen({ + currentStep, + bitcoinHandler, + psbtHandler, + ethereumHandler, +}: 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 { bitcoinPrice } = bitcoinHandler; + const { getProtocolFee } = ethereumHandler; + const { handleSignFundingTransaction } = psbtHandler; const [isSubmitting, setIsSubmitting] = useState(false); const [protocolFeePercentage, setProtocolFeePercentage] = useState(undefined); @@ -33,18 +42,18 @@ export function LockScreen({ currentStep }: LockScreenProps): React.JSX.Element useEffect(() => { const fetchProtocolFeePercentage = async () => { - const currentProtocolFeePercentage = await ethereum?.getProtocolFee(); + const currentProtocolFeePercentage = await getProtocolFee(); setProtocolFeePercentage(currentProtocolFeePercentage); }; fetchProtocolFeePercentage(); - }, [ethereum]); + }, [getProtocolFee]); async function handleClick(currentVault?: Vault) { if (!currentVault) return; try { setIsSubmitting(true); - await handleSignTransaction(currentVault.collateral, currentVault.uuid); + await handleSignFundingTransaction(currentVault.collateral, currentVault.uuid); setIsSubmitting(false); } catch (error) { setIsSubmitting(false); @@ -63,7 +72,7 @@ export function LockScreen({ currentStep }: LockScreenProps): React.JSX.Element - - - - - - - Funding Transaction - - - - - - Closing Transaction - - - - - ); } diff --git a/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx b/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx new file mode 100644 index 00000000..e435d3ca --- /dev/null +++ b/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx @@ -0,0 +1,94 @@ +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { Button, VStack, useToast } from '@chakra-ui/react'; +import { VaultCard } from '@components/vault/vault-card'; +import { UseBitcoinReturnType } from '@hooks/use-bitcoin'; +import { UseEthereumReturnType } from '@hooks/use-ethereum'; +import { UseSignPSBTReturnType } from '@hooks/use-psbt'; +import { useVaults } from '@hooks/use-vaults'; +import { BitcoinError } from '@models/error-types'; +import { Vault } from '@models/vault'; +import { mintUnmintActions } from '@store/slices/mintunmint/mintunmint.actions'; + +import { LockScreenProtocolFee } from './components/protocol-fee'; + +interface SignClosingTransactionScreenProps { + bitcoinHandler: UseBitcoinReturnType; + ethereumHandler: UseEthereumReturnType; + psbtHandler: UseSignPSBTReturnType; + currentStep: [number, string]; +} + +export function SignClosingTransactionScreen({ + currentStep, + bitcoinHandler, + ethereumHandler, + psbtHandler, +}: SignClosingTransactionScreenProps): React.JSX.Element { + const toast = useToast(); + const dispatch = useDispatch(); + + const { readyVaults } = useVaults(); + + const { bitcoinPrice } = bitcoinHandler; + const { getProtocolFee } = ethereumHandler; + const { handleSignClosingTransaction } = psbtHandler; + + const [isSubmitting, setIsSubmitting] = useState(false); + const [protocolFeePercentage, setProtocolFeePercentage] = useState(undefined); + + const currentVault = readyVaults.find(vault => vault.uuid === currentStep[1]); + + useEffect(() => { + const fetchProtocolFeePercentage = async () => { + const currentProtocolFeePercentage = await getProtocolFee(); + setProtocolFeePercentage(currentProtocolFeePercentage); + }; + fetchProtocolFeePercentage(); + }, [getProtocolFee]); + + async function handleClick(currentVault?: Vault) { + if (!currentVault) return; + + try { + setIsSubmitting(true); + await handleSignClosingTransaction(); + setIsSubmitting(false); + } catch (error) { + setIsSubmitting(false); + toast({ + title: 'Failed to sign transaction', + description: error instanceof BitcoinError ? error.message : '', + status: 'error', + duration: 9000, + isClosable: true, + }); + } + } + + return ( + + + + + + + ); +} diff --git a/src/app/components/mint-unmint/components/mint/mint.tsx b/src/app/components/mint-unmint/components/mint/mint.tsx index 72ed1577..c3459795 100644 --- a/src/app/components/mint-unmint/components/mint/mint.tsx +++ b/src/app/components/mint-unmint/components/mint/mint.tsx @@ -1,9 +1,12 @@ import { useSelector } from 'react-redux'; import { HStack } from '@chakra-ui/react'; +import { useBlockchainContext } from '@hooks/use-blockchain-context'; +import { usePSBT } from '@hooks/use-psbt'; import { RootState } from '@store/index'; import { LockScreen } from '../lock-screen/lock-screen'; +import { SignClosingTransactionScreen } from '../lock-screen/sign-closing-transaction-screen'; import { ProgressTimeline } from '../progress-timeline/progress-timeline'; import { TransactionForm } from '../transaction-form/transaction-form'; import { TransactionSummary } from '../transaction-summary/transaction-summary'; @@ -11,6 +14,9 @@ import { Walkthrough } from '../walkthrough/walkthrough'; import { MintLayout } from './components/mint.layout'; export function Mint(): React.JSX.Element { + const { bitcoin, ethereum } = useBlockchainContext(); + const psbtHandler = usePSBT(bitcoin); + const { mintStep } = useSelector((state: RootState) => state.mintunmint); return ( @@ -19,8 +25,23 @@ export function Mint(): React.JSX.Element { {[0].includes(mintStep[0]) && } - {[1].includes(mintStep[0]) && } - {[2, 3].includes(mintStep[0]) && ( + {[1].includes(mintStep[0]) && ( + + )} + {[2].includes(mintStep[0]) && ( + + )} + {[3, 4].includes(mintStep[0]) && ( )} diff --git a/src/app/components/mint-unmint/components/progress-timeline/components/progress-timeline-step.tsx b/src/app/components/mint-unmint/components/progress-timeline/components/progress-timeline-step.tsx index e5ea120c..92d88422 100644 --- a/src/app/components/mint-unmint/components/progress-timeline/components/progress-timeline-step.tsx +++ b/src/app/components/mint-unmint/components/progress-timeline/components/progress-timeline-step.tsx @@ -1,10 +1,12 @@ -import { Divider, Text } from '@chakra-ui/react'; +import { Divider, Stack, Text } from '@chakra-ui/react'; import { StepIconOne, StepIconThree, StepIconTwo } from '../../../../../../styles/icon'; interface StepProps { + width?: string; currentStep: number; stepIndex: number; + isFirstStep?: boolean; isLastStep?: boolean; title?: string; } @@ -48,15 +50,25 @@ export function StepGraphics({ ); } -export function StepText({ currentStep, stepIndex, title }: StepProps): React.JSX.Element { +export function StepText({ + currentStep, + stepIndex, + width, + title, + isFirstStep = false, + isLastStep = false, +}: StepProps): React.JSX.Element { return ( - = stepIndex ? 'accent.cyan.01' : 'white.01'} - fontSize={'xs'} - fontWeight={currentStep === stepIndex ? 800 : 400} - opacity={currentStep > stepIndex ? '50%' : '100%'} - > - {title} - + + = stepIndex ? 'accent.cyan.01' : 'white.01'} + fontSize={'xs'} + fontWeight={currentStep === stepIndex ? 800 : 400} + opacity={currentStep > stepIndex ? '50%' : '100%'} + > + {title} + + ); } diff --git a/src/app/components/mint-unmint/components/progress-timeline/progress-timeline.tsx b/src/app/components/mint-unmint/components/progress-timeline/progress-timeline.tsx index e648b4b3..e8db6180 100644 --- a/src/app/components/mint-unmint/components/progress-timeline/progress-timeline.tsx +++ b/src/app/components/mint-unmint/components/progress-timeline/progress-timeline.tsx @@ -31,12 +31,31 @@ export function ProgressTimeline({ - + + - - - + + + + ); diff --git a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx index 7ab2a4d0..1518eeea 100644 --- a/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx +++ b/src/app/components/mint-unmint/components/walkthrough/walkthrough.tsx @@ -68,6 +68,28 @@ export function Walkthrough({ flow, currentStep }: WalkthroughProps): React.JSX. ); case 2: + return ( + + + + Sign the closing transaction in your{' '} + + Bitcoin Wallet{' '} + + which will be broadcasted once the dlcBTC is redeemed. + + + ); + case 3: return ( ; diff --git a/src/app/hooks/use-bitcoin.ts b/src/app/hooks/use-bitcoin.ts index 40723c69..360469f5 100644 --- a/src/app/hooks/use-bitcoin.ts +++ b/src/app/hooks/use-bitcoin.ts @@ -6,6 +6,8 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; import { hex } from '@scure/base'; import * as btc from '@scure/btc-signer'; +import { useEndpoints } from './use-endpoints'; + const networkModes = ['mainnet', 'testnet'] as const; type NetworkModes = (typeof networkModes)[number]; @@ -91,16 +93,8 @@ export interface UseBitcoinReturnType { bitcoinPrice: number; } -// interface SignAndBroadcastFundingPSBTResult { -// fundingTransactionID: string; -// multisigTransaction: btc.P2TROut; -// userNativeSegwitAddress: string; -// btcAmount: number; -// } - -const ELECTRUM_API_URL = 'https://devnet.dlc.link/electrs'; - export function useBitcoin(): UseBitcoinReturnType { + const { attestorAPIURL, bitcoinBlockchainAPIURL } = useEndpoints(); const [bitcoinPrice, setBitcoinPrice] = useState(0); const [btcNetwork, setBTCNetwork] = useState(regtest); @@ -123,7 +117,7 @@ export function useBitcoin(): UseBitcoinReturnType { async function gatherUTXOs(bitcoinNativeSegwitAddress: BitcoinNativeSegwitAddress): Promise { const response = await fetch( - `${ELECTRUM_API_URL}/address/${bitcoinNativeSegwitAddress.address}/utxo` + `${bitcoinBlockchainAPIURL}/address/${bitcoinNativeSegwitAddress.address}/utxo` ); const allUTXOs = await response.json(); const userPublicKey = hexToBytes(bitcoinNativeSegwitAddress.publicKey); @@ -131,14 +125,13 @@ export function useBitcoin(): UseBitcoinReturnType { const utxos = await Promise.all( allUTXOs.map(async (utxo: UTXO) => { - const txHex = await (await fetch(`${ELECTRUM_API_URL}/tx/${utxo.txid}/hex`)).text(); + const txHex = await (await fetch(`${bitcoinBlockchainAPIURL}/tx/${utxo.txid}/hex`)).text(); return { ...spend, txid: utxo.txid, index: utxo.vout, value: utxo.value, nonWitnessUtxo: hex.decode(txHex), - // script: utxo.scriptpubkey, //do i need to handle when it is witness? how? }; }) ); @@ -147,9 +140,16 @@ export function useBitcoin(): UseBitcoinReturnType { } async function getAttestorPublicKey(): Promise { - const response = await fetch('http://localhost:3000/publickey'); - const attestorPublicKey = await response.text(); - return attestorPublicKey; + const attestorGetGroupPublicKeyURL = `${attestorAPIURL}/get-group-publickey`; + + try { + const response = await fetch(attestorGetGroupPublicKeyURL); + const attestorGroupPublicKey = await response.text(); + + return attestorGroupPublicKey; + } catch (error) { + throw new BitcoinError(`Error getting attestor public key: ${error}`); + } } function createMultisigTransactionAndAddress( @@ -196,30 +196,25 @@ 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; - // } - async function sendPSBT( closingPSBT: string, uuid: string, userNativeSegwitAddress: string ): Promise { setBTCNetwork(regtest); + const attestorCreatePSBTEventURL = `${attestorAPIURL}/create-psbt-event`; try { - const response = await fetch('http://localhost:3000/create-psbt-event', { + const response = await fetch(attestorCreatePSBTEventURL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ uuid, - closingPsbt: closingPSBT, - mintAddress: userNativeSegwitAddress, - chain: 'evm-sepolia' + closing_psbt: closingPSBT, + mint_address: userNativeSegwitAddress, + chain: 'evm-sepolia', }), }); - console.log('response', response); + console.log('response', await response.text()); } catch (error) { throw new BitcoinError(`Error sending PSBT: ${error}`); } @@ -232,7 +227,6 @@ export function useBitcoin(): UseBitcoinReturnType { btcNetwork: BitcoinNetwork ): Promise { const closingTransaction = new btc.Transaction({ PSBTVersion: 0 }); - const fundingInput = { txid: hexToBytes(fundingTransactionID), index: 0, @@ -240,7 +234,11 @@ export function useBitcoin(): UseBitcoinReturnType { ...multisigTransaction, }; closingTransaction.addInput(fundingInput); - closingTransaction.addOutputAddress(userNativeSegwitAddress, BigInt(btcAmount - 10000), btcNetwork); + closingTransaction.addOutputAddress( + userNativeSegwitAddress, + BigInt(btcAmount - 10000), + btcNetwork + ); const closingPSBT = closingTransaction.toPSBT(); return closingPSBT; } @@ -274,7 +272,7 @@ export function useBitcoin(): UseBitcoinReturnType { transaction.finalize(); let fundingTransactionID = ''; - await fetch(`${ELECTRUM_API_URL}/tx`, { + await fetch(`${bitcoinBlockchainAPIURL}/tx`, { method: 'POST', body: bytesToHex(transaction.extract()), }).then(async response => { @@ -318,11 +316,6 @@ export function useBitcoin(): UseBitcoinReturnType { const attestorPublicKey = await getAttestorPublicKey(); - // const preImage = hexToBytes('107661134f21fc7c02223d50ab9eb3600bc3ffc3712423a1e47bb1f9a9dbf55f'); - // const preImageHash = hexToBytes( - // '6c60f404f8167a38fc70eaf8aa17ac351023bef86bcb9d1086a19afe95bd5333' - // ); - const userUTXOs = await gatherUTXOs(userAddresses[0] as BitcoinNativeSegwitAddress); const { multisigTransaction, multisigAddress } = createMultisigTransactionAndAddress( hex.decode(userPublicKey), diff --git a/src/app/hooks/use-blockchain-context.ts b/src/app/hooks/use-blockchain-context.ts new file mode 100644 index 00000000..e66061bc --- /dev/null +++ b/src/app/hooks/use-blockchain-context.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import { BlockchainContext, BlockchainContextType } from '@providers/blockchain-context-provider'; + +export function useBlockchainContext(): BlockchainContextType { + const blockchainContext = useContext(BlockchainContext); + + if (!blockchainContext) { + throw new Error( + 'Blockchain Context not found. Make sure you are using the BlockchainContextProvider.' + ); + } + + return blockchainContext; +} diff --git a/src/app/hooks/use-confirmation-checker.ts b/src/app/hooks/use-confirmation-checker.ts index 3074d7ef..4b24bff2 100644 --- a/src/app/hooks/use-confirmation-checker.ts +++ b/src/app/hooks/use-confirmation-checker.ts @@ -1,11 +1,9 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { VaultState } from '@models/vault'; +import { Vault, VaultState } from '@models/vault'; -export function useConfirmationChecker( - txID: string | undefined, - vaultState: VaultState | undefined -): number { +export function useConfirmationChecker(vault?: Vault): number { + const txID = vault?.state === VaultState.FUNDING ? vault?.fundingTX : vault?.closingTX; const bitcoinExplorerTXURL = `https://devnet.dlc.link/electrs/tx/${txID}`; const bitcoinExplorerHeightURL = `https://devnet.dlc.link/electrs/blocks/tip/height`; const fetchInterval = useRef(undefined); @@ -13,9 +11,10 @@ export function useConfirmationChecker( const [transactionProgress, setTransactionProgress] = useState(0); const memoizedTransactionProgress = useMemo(() => transactionProgress, [transactionProgress]); + console.log('vault?.state', vault); const fetchTransactionDetails = async () => { - if (!txID || (vaultState && ![VaultState.FUNDING, VaultState.CLOSED].includes(vaultState))) { + if (!txID || (vault?.state && ![VaultState.FUNDING, VaultState.CLOSED].includes(vault.state))) { clearInterval(fetchInterval.current); return; } @@ -31,7 +30,8 @@ export function useConfirmationChecker( } let bitcoinTransactionBlockHeight; - + console.log(vault?.closingTX); + console.log(bitcoinExplorerTXURL); try { const response = await fetch(bitcoinExplorerTXURL, { headers: { Accept: 'application/json' }, @@ -56,7 +56,7 @@ export function useConfirmationChecker( useEffect(() => { fetchInterval.current = setInterval(fetchTransactionDetails, 10000) as unknown as number; // Cleanup the interval when the component unmounts return () => clearInterval(fetchInterval.current); - }, [vaultState, txID]); + }, [vault?.state, txID]); return memoizedTransactionProgress; } diff --git a/src/app/hooks/use-endpoints.ts b/src/app/hooks/use-endpoints.ts index cd287b2f..2c1e72a4 100644 --- a/src/app/hooks/use-endpoints.ts +++ b/src/app/hooks/use-endpoints.ts @@ -4,58 +4,63 @@ import { useSelector } from 'react-redux'; import { EthereumNetwork } from '@models/network'; import { RootState } from '@store/index'; -interface UseEndpointsReturnType { - routerWalletURL: string | undefined; - ethereumExplorerAPIURL: string | undefined; - bitcoinExplorerAPIURL: string | undefined; +interface NetworkEndpoints { + attestorAPIURL: string; + ethereumExplorerAPIURL: string; + bitcoinExplorerAPIURL: string; + bitcoinBlockchainAPIURL: string; } -export function useEndpoints(): UseEndpointsReturnType { +export function useEndpoints(): NetworkEndpoints { const { network } = useSelector((state: RootState) => state.account); - const [routerWalletURL, setRouterWalletURL] = useState(undefined); - const [ethereumExplorerAPIURL, setEthereumExplorerAPIURL] = useState( - undefined - ); + const [attestorAPIURL, setAttestorAPIURL] = useState(''); + const [ethereumExplorerAPIURL, setEthereumExplorerAPIURL] = useState(''); const [bitcoinExplorerAPIURL, setBitcoinExplorerAPIURL] = useState(''); + const [bitcoinBlockchainAPIURL, setBitcoinBlockchainAPIURL] = useState(''); useEffect(() => { if (!network) return; - const { routerWalletURL, ethereumExplorerAPIURL, bitcoinExplorerAPIURL } = getEndpoints(); + const { + attestorAPIURL, + ethereumExplorerAPIURL, + bitcoinExplorerAPIURL, + bitcoinBlockchainAPIURL, + } = getEndpoints(); - setRouterWalletURL(routerWalletURL); + setAttestorAPIURL(attestorAPIURL); setEthereumExplorerAPIURL(ethereumExplorerAPIURL); setBitcoinExplorerAPIURL(bitcoinExplorerAPIURL); + setBitcoinBlockchainAPIURL(bitcoinBlockchainAPIURL); }, [network]); - function getEndpoints(): { - routerWalletURL: string; - ethereumExplorerAPIURL: string; - bitcoinExplorerAPIURL: string; - } { + function getEndpoints(): NetworkEndpoints { switch (network?.id) { case EthereumNetwork.Sepolia: return { - routerWalletURL: 'https://devnet.dlc.link/wallet', + attestorAPIURL: 'http://localhost:8811', ethereumExplorerAPIURL: 'https://sepolia.etherscan.io/tx/', bitcoinExplorerAPIURL: 'http://devnet.dlc.link/electrs/tx/', + bitcoinBlockchainAPIURL: 'https://devnet.dlc.link/electrs', }; case EthereumNetwork.Goerli: return { - routerWalletURL: 'https://devnet.dlc.link/wallet', + attestorAPIURL: 'http://localhost:8811', ethereumExplorerAPIURL: 'https://goerli.etherscan.io/tx/', bitcoinExplorerAPIURL: 'https://blockstream.info/testnet/tx/', + bitcoinBlockchainAPIURL: 'https://devnet.dlc.link/electrs', }; case EthereumNetwork.X1Testnet: return { - routerWalletURL: 'https://devnet.dlc.link/wallet', + attestorAPIURL: 'http://localhost:8811', ethereumExplorerAPIURL: 'https://www.oklink.com/x1-test/tx/', bitcoinExplorerAPIURL: 'http://devnet.dlc.link/electrs/tx/', + bitcoinBlockchainAPIURL: 'https://devnet.dlc.link/electrs', }; default: throw new Error(`Unsupported network: ${network?.name}`); } } - return { routerWalletURL, ethereumExplorerAPIURL, bitcoinExplorerAPIURL }; + return { attestorAPIURL, ethereumExplorerAPIURL, bitcoinExplorerAPIURL, bitcoinBlockchainAPIURL }; } diff --git a/src/app/hooks/use-psbt.ts b/src/app/hooks/use-psbt.ts new file mode 100644 index 00000000..5a259d39 --- /dev/null +++ b/src/app/hooks/use-psbt.ts @@ -0,0 +1,92 @@ +import { 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 { + handleSignFundingTransaction: (btcAmount: number, vaultUUID: string) => Promise; + handleSignClosingTransaction: () => Promise; +} + +export function usePSBT(bitcoinHandler: UseBitcoinReturnType): UseSignPSBTReturnType { + const dispatch = useDispatch(); + const { network } = useSelector((state: RootState) => state.account); + + const { signAndBroadcastFundingPSBT, signClosingPSBT } = bitcoinHandler; + + const [vaultUUID, setVaultUUID] = useState(); + const [bitcoinAmount, setBitcoinAmount] = useState(); + + const [userNativeSegwitAddress, setUserNativeSegwitAddress] = useState(); + const [fundingTransactionID, setFundingTransactionID] = useState(); + const [multisigTransaction, setMultisigTransaction] = useState(); + + async function handleSignFundingTransaction( + bitcoinAmount: number, + vaultUUID: string + ): Promise { + const shiftedBTCDepositAmount = customShiftValue(bitcoinAmount, 8, false); + + try { + const { fundingTransactionID, multisigTransaction, userNativeSegwitAddress } = + await signAndBroadcastFundingPSBT(shiftedBTCDepositAmount); + + setVaultUUID(vaultUUID); + setBitcoinAmount(shiftedBTCDepositAmount); + setUserNativeSegwitAddress(userNativeSegwitAddress); + setFundingTransactionID(fundingTransactionID); + setMultisigTransaction(multisigTransaction); + + dispatch(mintUnmintActions.setMintStep([2, vaultUUID])); + return fundingTransactionID; + } catch (error) { + throw new BitcoinError(`Error signing funding transaction: ${error}`); + } + } + + async function handleSignClosingTransaction() { + try { + if ( + !fundingTransactionID || + !multisigTransaction || + !userNativeSegwitAddress || + !bitcoinAmount || + !vaultUUID || + !network + ) { + throw new Error( + 'Funding transaction must be signed before closing transaction can be signed' + ); + } + await signClosingPSBT( + fundingTransactionID, + multisigTransaction, + vaultUUID, + userNativeSegwitAddress, + bitcoinAmount + ); + dispatch( + vaultActions.setVaultToFunding({ + vaultUUID, + fundingTX: fundingTransactionID, + networkID: network.id, + }) + ); + dispatch(mintUnmintActions.setMintStep([3, vaultUUID])); + } catch (error) { + throw new BitcoinError(`Error signing closing transaction: ${error}`); + } + } + + return { + handleSignFundingTransaction, + handleSignClosingTransaction, + }; +} diff --git a/src/app/hooks/use-sign-psbt.ts b/src/app/hooks/use-sign-psbt.ts deleted file mode 100644 index a13f998e..00000000 --- a/src/app/hooks/use-sign-psbt.ts +++ /dev/null @@ -1,129 +0,0 @@ -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'; - -interface UseSignPSBTReturnType { - handleSignTransaction: (btcAmount: number, vaultUUID: string) => Promise; - fundingTransactionSigned: boolean; - closingTransactionSigned: boolean; -} - -export function useSignPSBT(useBitcoin?: UseBitcoinReturnType): UseSignPSBTReturnType { - 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(); - const [multisigTransaction, setMultisigTransaction] = useState(); - const [userNativeSegwitAddress, setUserNativeSegwitAddress] = useState(); - - useEffect(() => { - 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): Promise { - try { - const { fundingTransactionID, multisigTransaction, userNativeSegwitAddress } = - await signAndBroadcastFundingPSBT(btcAmount); - setBTCAmount(btcAmount); - setFundingTransactionID(fundingTransactionID); - setMultisigTransaction(multisigTransaction); - setUserNativeSegwitAddress(userNativeSegwitAddress); - setFundingTransactionSigned(true); - return fundingTransactionID; - } catch (error) { - throw new BitcoinError(`Error signing funding transaction: ${error}`); - } - } - - async function handleSignClosingTransaction() { - try { - if ( - !fundingTransactionID || - !multisigTransaction || - !userNativeSegwitAddress || - !btcAmount || - !vaultUUID - ) { - throw new Error( - 'Funding transaction must be signed before closing transaction can be signed' - ); - } - await signClosingPSBT( - fundingTransactionID, - multisigTransaction, - vaultUUID, - userNativeSegwitAddress, - btcAmount - ); - setClosingTransactionSigned(true); - } catch (error) { - throw new BitcoinError(`Error signing closing transaction: ${error}`); - } - } - - async function handleSignTransaction(btcAmount: number, vaultUUID: string) { - const shiftedBTCDepositAmount = customShiftValue(btcAmount, 8, false); - setBTCAmount(shiftedBTCDepositAmount); - setVaultUUID(vaultUUID); - if (!fundingTransactionSigned) { - await handleSignFundingTransaction(shiftedBTCDepositAmount); - } else { - await handleSignClosingTransaction(); - if (fundingTransactionID && network && vaultUUID) { - dispatch( - vaultActions.setVaultToFunding({ - vaultUUID, - fundingTX: fundingTransactionID, - networkID: network.id, - }) - ); - dispatch(mintUnmintActions.setMintStep([2, vaultUUID])); - } - } - } - - return { handleSignTransaction, fundingTransactionSigned, closingTransactionSigned }; -} diff --git a/src/app/providers/blockchain-context-provider.tsx b/src/app/providers/blockchain-context-provider.tsx index 31ffc59d..d3d32ada 100644 --- a/src/app/providers/blockchain-context-provider.tsx +++ b/src/app/providers/blockchain-context-provider.tsx @@ -5,7 +5,7 @@ import { UseEthereumReturnType, useEthereum } from '@hooks/use-ethereum'; import { useObserver } from '@hooks/use-observer'; import { HasChildren } from '@models/has-children'; -interface BlockchainContextType { +export interface BlockchainContextType { ethereum: UseEthereumReturnType; bitcoin: UseBitcoinReturnType; } From bfa231dc96e90818d7103fb7ba61b1fa4954dadc Mon Sep 17 00:00:00 2001 From: Polybius93 Date: Fri, 1 Mar 2024 10:43:13 +0100 Subject: [PATCH 09/14] feat: modify blockchain context retrieval --- .../sign-closing-transaction-screen.tsx | 2 +- .../progress-timeline/progress-timeline.tsx | 6 +++--- .../protocol-summary-stack.tsx | 12 +++++++----- .../transaction-form/transaction-form.tsx | 14 ++++++++------ .../components/walkthrough/walkthrough.tsx | 12 ++++++------ .../components/vaults-list-group-container.tsx | 10 +++++----- src/app/hooks/use-bitcoin.ts | 3 +-- src/app/hooks/use-confirmation-checker.ts | 4 +--- src/app/providers/balance-context-provider.tsx | 15 +++++++-------- src/app/providers/vault-context-provider.tsx | 10 ++++++---- 10 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx b/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx index e435d3ca..11a39531 100644 --- a/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx +++ b/src/app/components/mint-unmint/components/lock-screen/sign-closing-transaction-screen.tsx @@ -80,7 +80,7 @@ export function SignClosingTransactionScreen({ variant={'account'} onClick={() => handleClick(currentVault)} > - Sign Closing Transaction + Sign Closing TX