Skip to content

Commit

Permalink
Spend to Taproot (#91)
Browse files Browse the repository at this point in the history
* adds new methods in @caravan/psbt to use bitcoinjs-lib v6 to support spending to taproot outputs
* uses these new functions in caravan coordinator and caravan wallets

Other fixes include:
* don't polyfill process env for trezor environment variables in @caravan/wallets
* cleanup some dependencies in @caravan/wallets
* remove unexported PSBTv0 modules from @caravan/psbt
* some hidden regtest support via configs
* jest and other tooling cleanup
  • Loading branch information
bucko13 authored Jun 6, 2024
1 parent 214d221 commit 0d81717
Show file tree
Hide file tree
Showing 123 changed files with 40,811 additions and 2,506 deletions.
5 changes: 5 additions & 0 deletions .changeset/bright-dogs-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caravan/psbt": minor
---

export of new utils for psbt v0 handling. Primarily adds support for taproot outputs by upgrading to bitcoinjs-lib v6 depedency. Upgrades to a new API from legacy utils from caravan/bitcoin and includes some utilities and types for handling conversions.
5 changes: 5 additions & 0 deletions .changeset/calm-adults-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caravan/bitcoin": minor
---

export signature utilities from caravan/bitcoin to support new psbt tooling
5 changes: 5 additions & 0 deletions .changeset/cyan-queens-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caravan/wallets": minor
---

upgrading psbt generation to support taproot outputs and new caravan/psbt utils
5 changes: 5 additions & 0 deletions .changeset/happy-rivers-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caravan/multisig": major
---

New package for multisig wallet utilities and types to share across other packages
5 changes: 5 additions & 0 deletions .changeset/light-plants-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caravan/bitcoin": minor
---

transaction parser was stripping out network information from global xpubs being added to psbt. global xpubs will now respect the network and include appropriate prefix
5 changes: 5 additions & 0 deletions .changeset/perfect-panthers-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"caravan-coordinator": minor
---

upgrade tx processing utils to use new psbt utils for taproot output support
6 changes: 6 additions & 0 deletions .changeset/twelve-kids-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@caravan/wallets": patch
"caravan-coordinator": patch
---

fixes an issue where esbuild was polyfilling process.env which breaks the ability to override trezor connect settings and pass other env vars at run time in dependent applications
5 changes: 5 additions & 0 deletions .changeset/wet-cougars-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@caravan/eslint-config": patch
---

initiliazing package, upgrading deps. need to improve shared config before major release
4 changes: 4 additions & 0 deletions apps/coordinator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ By default, Caravan uses a free API provided by
information about the bitcoin blockchain or to broadcast transactions. Blockstream.info is also available as a fallback
option for a public API.

Mainnet and Testnet are available options for connecting to any of the available
consensus client options. Regtest can be available through an uploaded wallet
configuration file, but only for the private client backend.

### Bitcoind client

You can also ask Caravan to use your own private [bitcoind full
Expand Down
25 changes: 0 additions & 25 deletions apps/coordinator/jest.config.json

This file was deleted.

27 changes: 27 additions & 0 deletions apps/coordinator/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { JestConfigWithTsJest } from "ts-jest";

const config: JestConfigWithTsJest = {
testEnvironment: "jsdom",
transform: {
"\\.[jt]sx?$": "babel-jest",
},
transformIgnorePatterns: ["^.+\\.module\\.(css|sass|scss)$"],
moduleNameMapper: {
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
},
moduleFileExtensions: [
"web.cjs",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.cjsx",
"jsx",
"node",
],
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
};

export default config;
1 change: 1 addition & 0 deletions apps/coordinator/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@inrupt/jest-jsdom-polyfills";
4 changes: 3 additions & 1 deletion apps/coordinator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@inrupt/jest-jsdom-polyfills": "^3.2.1",
"@testing-library/jest-dom": "^5.6.0",
"@testing-library/react": "^10.0.4",
"@types/history": "^5.0.0",
Expand Down Expand Up @@ -95,8 +96,9 @@
"dependencies": {
"@caravan/bitcoin": "*",
"@caravan/clients": "*",
"@caravan/descriptors": "^0.0.6",
"@caravan/descriptors": "^0.1.1",
"@caravan/eslint-config": "*",
"@caravan/psbt": "*",
"@caravan/typescript-config": "*",
"@caravan/wallets": "*",
"@emotion/react": "^11.10.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import PropTypes from "prop-types";
import { connect } from "react-redux";
import {
validateHex,
validateMultisigSignature,
multisigBIP32Path,
multisigBIP32Root,
validateBIP32Path,
Expand Down Expand Up @@ -38,6 +37,12 @@ import {
} from "../../actions/signatureImporterActions";
import { setSigningKey as setSigningKeyAction } from "../../actions/transactionActions";
import { downloadFile } from "../../utils";
import {
convertLegacyInput,
convertLegacyOutput,
getUnsignedMultisigPsbtV0,
validateMultisigPsbtSignature,
} from "@caravan/psbt";

const TEXT = "text";
const UNKNOWN = "unknown";
Expand Down Expand Up @@ -389,12 +394,17 @@ class SignatureImporter extends React.Component {

let publicKey;
try {
publicKey = validateMultisigSignature(
const args = {
network,
inputs,
outputs,
inputs: inputs.map(convertLegacyInput),
outputs: outputs.map(convertLegacyOutput),
};
const psbt = getUnsignedMultisigPsbtV0(args);
publicKey = validateMultisigPsbtSignature(
psbt.toBase64(),
inputIndex,
inputSignature,
inputs[inputIndex].amountSats,
);
} catch (e) {
errback(`Signature for input ${inputNumber} is invalid.`);
Expand Down Expand Up @@ -482,13 +492,17 @@ class SignatureImporter extends React.Component {
return;
}
try {
// This returns false if it completes with no error
publicKey = validateMultisigSignature(
const args = {
network,
inputs,
outputs,
inputs: inputs.map(convertLegacyInput),
outputs: outputs.map(convertLegacyOutput),
};
const psbt = getUnsignedMultisigPsbtV0(args);
publicKey = validateMultisigPsbtSignature(
psbt.toBase64(),
inputIndex,
inputSignature,
inputs[inputIndex].amountSats,
);
} catch (e) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -579,7 +593,8 @@ SignatureImporter.propTypes = {
}),
).isRequired,
fee: PropTypes.string.isRequired,
inputs: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
inputs: PropTypes.arrayOf(PropTypes.shape({ amountSats: PropTypes.string }))
.isRequired,
inputsTotalSats: PropTypes.shape({}).isRequired,
isWallet: PropTypes.bool.isRequired,
network: PropTypes.string.isRequired,
Expand Down
48 changes: 35 additions & 13 deletions apps/coordinator/src/components/ScriptExplorer/Transaction.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import {
signedMultisigTransaction,
blockExplorerTransactionURL,
addSignaturesToPSBT,
} from "@caravan/bitcoin";

import {
Expand All @@ -21,6 +21,13 @@ import { updateBlockchainClient } from "../../actions/clientActions";
import Copyable from "../Copyable";
import { externalLink } from "utils/ExternalLink";
import { setTXID } from "../../actions/transactionActions";
import {
convertLegacyInput,
convertLegacyOutput,
getUnsignedMultisigPsbtV0,
} from "@caravan/psbt";
import { Psbt } from "bitcoinjs-lib";
import { Buffer } from "buffer";

class Transaction extends React.Component {
constructor(props) {
Expand All @@ -34,14 +41,30 @@ class Transaction extends React.Component {

buildSignedTransaction = () => {
const { network, inputs, outputs, signatureImporters } = this.props;
return signedMultisigTransaction(
const args = {
network,
inputs,
outputs,
Object.values(signatureImporters).map(
(signatureImporter) => signatureImporter.signature,
),
);
inputs: inputs.map(convertLegacyInput),
outputs: outputs.map(convertLegacyOutput),
};
const psbt = getUnsignedMultisigPsbtV0(args);
let partiallySignedTransaction = psbt.toBase64();
for (const signatureImporter of Object.values(signatureImporters)) {
partiallySignedTransaction = addSignaturesToPSBT(
network,
partiallySignedTransaction,
signatureImporter.publicKeys.map((pubkey) =>
Buffer.from(pubkey, "hex"),
),
signatureImporter.signature.map((signature) =>
Buffer.from(signature, "hex"),
),
);
}

return Psbt.fromBase64(partiallySignedTransaction)
.finalizeAllInputs()
.extractTransaction()
.toHex();
};

handleBroadcast = async () => {
Expand All @@ -52,7 +75,7 @@ class Transaction extends React.Component {
let txid = "";
this.setState({ broadcasting: true });
try {
txid = await client.broadcastTransaction(signedTransaction.toHex());
txid = await client.broadcastTransaction(signedTransaction);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
Expand All @@ -71,14 +94,13 @@ class Transaction extends React.Component {

render() {
const { error, broadcasting, txid } = this.state;
const signedTransaction = this.buildSignedTransaction();
const signedTransactionHex = signedTransaction.toHex();
const signedTransactionHex = this.buildSignedTransaction();
return (
<Card>
<CardHeader title="Broadcast" />
<CardContent>
<form>
{signedTransaction && (
{signedTransactionHex && (
<Box mt={4}>
<Typography variant="h6">Signed Transaction</Typography>
<Copyable text={signedTransactionHex} code showIcon />
Expand All @@ -89,7 +111,7 @@ class Transaction extends React.Component {
<Button
variant="contained"
color="primary"
disabled={!signedTransaction || broadcasting}
disabled={!signedTransactionHex || broadcasting}
onClick={this.handleBroadcast}
>
Broadcast Transaction
Expand Down
6 changes: 1 addition & 5 deletions apps/coordinator/src/components/Wallet/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,7 @@ class CreateWallet extends React.Component {
setTotalSigners(walletConfiguration.quorum.totalSigners);
setRequiredSigners(walletConfiguration.quorum.requiredSigners);
setAddressType(walletConfiguration.addressType);
if (walletConfiguration.network === "regtest") {
setNetwork("testnet");
} else {
setNetwork(walletConfiguration.network);
}
setNetwork(walletConfiguration.network);
updateWalletNameAction(0, walletConfiguration.name);
updateWalletUuid(walletConfiguration.uuid);

Expand Down
26 changes: 16 additions & 10 deletions apps/coordinator/src/reducers/transactionReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import {
satoshisToBitcoins,
bitcoinsToSatoshis,
validateAddress,
unsignedMultisigTransaction,
unsignedMultisigPSBT,
unsignedTransactionObjectFromPSBT,
checkFeeRateError,
getFeeErrorMessage,
FeeValidationError,
unsignedMultisigTransaction,
} from "@caravan/bitcoin";
import {
convertLegacyInput,
convertLegacyOutput,
getUnsignedMultisigPsbtV0,
} from "@caravan/psbt";
import updateState from "./utils";
import { SET_NETWORK, SET_ADDRESS_TYPE } from "../actions/settingsActions";
import {
Expand Down Expand Up @@ -45,6 +48,7 @@ import {
SPEND_STEP_CREATE,
} from "../actions/transactionActions";
import { RESET_NODES_SPEND } from "../actions/walletActions";
import { Transaction } from "bitcoinjs-lib";

function sortInputs(a, b) {
const x = a.txid.toLowerCase();
Expand Down Expand Up @@ -280,16 +284,18 @@ function finalizeOutputs(state, action) {
// First try to build the transaction via PSBT, if that fails (e.g. an input doesn't know about its braid),
// then try to build it using the old TransactionBuilder plumbing.
try {
const unsignedTransactionPSBT = unsignedMultisigPSBT(
state.network,
state.inputs,
state.outputs,
);
unsignedTransaction = unsignedTransactionObjectFromPSBT(
unsignedTransactionPSBT,
const args = {
network: state.network,
inputs: state.inputs.map(convertLegacyInput),
outputs: state.outputs.map(convertLegacyOutput),
};
const psbt = getUnsignedMultisigPsbtV0(args);
unsignedTransaction = Transaction.fromHex(
psbt.data.globalMap.unsignedTx.toBuffer().toString("hex"),
);
} catch (e) {
// probably has an input that isn't braid aware.
// NOTE: This won't work for txs with taproot outputs
unsignedTransaction = unsignedMultisigTransaction(
state.network,
state.inputs,
Expand Down
1 change: 1 addition & 0 deletions apps/coordinator/src/reducers/transactionReducer.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "@testing-library/jest-dom";
import BigNumber from "bignumber.js";
import {
P2WSH,
Expand Down
7 changes: 2 additions & 5 deletions apps/coordinator/src/tests/addresses.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import React from "react";

import { blockExplorerAddressURL, TEST_FIXTURES } from "@caravan/bitcoin";
import {
ConfirmMultisigAddress,
LEDGER,
braidDetailsToWalletConfig,
} from "@caravan/wallets";
import { ConfirmMultisigAddress, LEDGER } from "@caravan/wallets";
import { Box, Table, TableBody, TableRow, TableCell } from "@mui/material";
import { externalLink } from "utils/ExternalLink";

import Test from "./Test";
import { braidDetailsToWalletConfig } from "@caravan/multisig";

class ConfirmMultisigAddressTest extends Test {
name() {
Expand Down
Loading

0 comments on commit 0d81717

Please sign in to comment.