Skip to content
This repository has been archived by the owner on Apr 25, 2024. It is now read-only.

Commit

Permalink
fix(wallet): adds utxo selection to manual spend on wallet interface (#…
Browse files Browse the repository at this point in the history
…223)

* fix(wallet): adds utxo selection to manual spend on wallet interface

* fix(wallet): pr review feedback

* fix(wallet): clears everything on edit transaction

* fix(wallet): fixes single utxo selection bug

Co-authored-by: Walden Raines <[email protected]>
  • Loading branch information
humanumbrella and waldenraines authored Jul 28, 2021
1 parent 77ae07f commit ef4ab05
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 43 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,6 @@
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"unchained-bitcoin": "^0.1.6",
"unchained-wallets": "^0.1.20"
"unchained-wallets": "^0.1.21"
}
}
142 changes: 121 additions & 21 deletions src/components/ScriptExplorer/UTXOSet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,42 +32,94 @@ class UTXOSet extends React.Component {
super(props);
this.state = {
inputsSatsSelected: props.inputsTotalSats,
inputs: props.inputs.map((input) => {
localInputs: props.inputs.map((input) => {
return {
...input,
checked: true,
checked: props.selectAll,
};
}),
toggleAll: true,
};
}

componentDidUpdate(prevProps) {
// This function exists because we need to respond to the parent node having
// its select/spend checkbox clicked (toggling select-all or select-none).
// None of this needs to happen on the redeem script interface.
const { multisig } = this.props;
if (multisig) {
const { node, existingTransactionInputs } = this.props;
const { localInputs } = this.state;
const prevMyInputsBeingSpent = this.filterInputs(
localInputs,
prevProps.existingTransactionInputs,
true
).length;
const myInputsBeingSpent = this.filterInputs(
localInputs,
existingTransactionInputs,
true
).length;

const isFullSpend = myInputsBeingSpent === localInputs.length;

// If the spend bool on the node changes, toggleAll the checks.
// but that's not quite enough because if a single UTXO is selected
// then it is also marked from not spend -> spend ... so don't want
// to toggleAll in that case. Furthermore, if you have 5 UTXOs and
// 2 selected and *then* click select all ... we also need to toggelAll.
if (
(prevProps.node.spend !== node.spend ||
myInputsBeingSpent !== prevMyInputsBeingSpent) &&
isFullSpend
) {
this.toggleAll(node.spend);
}
}
}

filterInputs = (localInputs, transactionStoreInputs, filterToMyInputs) => {
return localInputs.filter((input) => {
const included = transactionStoreInputs.filter((utxo) => {
return utxo.txid === input.txid && utxo.index === input.index;
});
return filterToMyInputs ? included.length > 0 : included.length === 0;
});
};

toggleInput = (inputIndex) => {
const { inputs } = this.state;
const { localInputs } = this.state;
this.setState({ toggleAll: false });

inputs[inputIndex].checked = !inputs[inputIndex].checked;
localInputs[inputIndex].checked = !localInputs[inputIndex].checked;

this.setInputsAndUpdateDisplay(inputs);
this.setInputsAndUpdateDisplay(localInputs);
};

toggleAll = () => {
const { inputs, toggleAll } = this.state;
toggleAll = (setTo = null) => {
const { localInputs, toggleAll } = this.state;
const toggled = !toggleAll;

inputs.forEach((input) => {
localInputs.forEach((input) => {
const i = input;
i.checked = toggled;
i.checked = setTo === null ? toggled : setTo;
return i;
});

this.setInputsAndUpdateDisplay(inputs);
this.setInputsAndUpdateDisplay(localInputs);
this.setState({ toggleAll: toggled });
};

setInputsAndUpdateDisplay = (inputs) => {
const { setInputs, multisig, bip32Path } = this.props;
let inputsToSpend = inputs.filter((input) => input.checked);
setInputsAndUpdateDisplay = (incomingInputs) => {
const {
setInputs,
multisig,
bip32Path,
existingTransactionInputs,
setSpendCheckbox,
} = this.props;
const { localInputs } = this.state;
let inputsToSpend = incomingInputs.filter((input) => input.checked);
if (multisig) {
inputsToSpend = inputsToSpend.map((utxo) => {
return { ...utxo, multisig, bip32Path };
Expand All @@ -80,15 +132,49 @@ class UTXOSet extends React.Component {
this.setState({
inputsSatsSelected: satsSelected,
});
if (inputsToSpend.length > 0) {
setInputs(inputsToSpend);
let totalInputsToSpend = inputsToSpend;

// The following is only relevant on the wallet interface
if (multisig) {
// There are 3 total sets of inputs to care about:
// 1. localInputs - all inputs from this node/address
// 2. inputsToSpend - equal to or subset of those from #1 (inputs marked checked==true)
// 3. existingTransactionInputs - all inputs from all nodes/addresses

// Check if #3 contains any inputs not associated with this component
const notMyInputs = this.filterInputs(
existingTransactionInputs,
localInputs,
false
);

if (notMyInputs.length > 0) {
totalInputsToSpend = inputsToSpend.concat(notMyInputs);
}

// Now we push a change up to the top level node so it can update its checkbox
const numLocalInputsToSpend = inputsToSpend.length;
if (numLocalInputsToSpend === 0) {
setSpendCheckbox(false);
} else if (numLocalInputsToSpend < localInputs.length) {
setSpendCheckbox("indeterminate");
} else {
setSpendCheckbox(true);
}
}

if (totalInputsToSpend.length > 0) {
setInputs(totalInputsToSpend);
} else if (multisig) {
// If we do this on redeem script interface, the panel will disappear
setInputs([]);
}
};

renderInputs = () => {
const { network, showSelection, finalizedOutputs } = this.props;
const { inputs } = this.state;
return inputs.map((input, inputIndex) => {
const { localInputs } = this.state;
return localInputs.map((input, inputIndex) => {
const confirmedStyle = `${styles.utxoTxid}${
input.confirmed ? "" : ` ${styles.unconfirmed}`
}`;
Expand Down Expand Up @@ -131,22 +217,22 @@ class UTXOSet extends React.Component {

render() {
const {
inputs,
inputsTotalSats,
showSelection = true,
hideSelectAllInHeader,
finalizedOutputs,
} = this.props;
const { inputsSatsSelected, toggleAll } = this.state;
const { inputsSatsSelected, toggleAll, localInputs } = this.state;
return (
<>
<Typography variant="h5">
{`Available Inputs (${inputs.length})`}{" "}
{`Available Inputs (${localInputs.length})`}{" "}
</Typography>
<p>The following UTXOs will be spent as inputs in a new transaction.</p>
<Table>
<TableHead>
<TableRow hover>
{showSelection && (
{showSelection && !hideSelectAllInHeader && (
<TableCell>
<Checkbox
data-testid="utxo-check-all"
Expand All @@ -157,6 +243,7 @@ class UTXOSet extends React.Component {
/>
</TableCell>
)}
{hideSelectAllInHeader && <TableCell />}
<TableCell>Number</TableCell>
<TableCell>TXID</TableCell>
<TableCell>Index</TableCell>
Expand Down Expand Up @@ -189,19 +276,32 @@ UTXOSet.propTypes = {
multisig: PropTypes.oneOfType([PropTypes.shape({}), PropTypes.bool]),
bip32Path: PropTypes.string,
showSelection: PropTypes.bool,
hideSelectAllInHeader: PropTypes.bool,
selectAll: PropTypes.bool,
finalizedOutputs: PropTypes.bool.isRequired,
node: PropTypes.shape({
spend: PropTypes.bool,
}),
existingTransactionInputs: PropTypes.arrayOf(PropTypes.shape({})),
setSpendCheckbox: PropTypes.func,
};

UTXOSet.defaultProps = {
multisig: false,
bip32Path: "",
showSelection: true,
hideSelectAllInHeader: false,
selectAll: true,
node: {},
existingTransactionInputs: [],
setSpendCheckbox: () => {},
};

function mapStateToProps(state) {
return {
...state.settings,
finalizedOutputs: state.spend.transaction.finalizedOutputs,
existingTransactionInputs: state.spend.transaction.inputs,
};
}

Expand Down
13 changes: 10 additions & 3 deletions src/components/Wallet/AddressExpander.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ class AddressExpander extends React.Component {
};

expandContent = () => {
const { client, node } = this.props;
const { utxos, balanceSats, multisig, bip32Path } = node;
const { client, node, setSpendCheckbox } = this.props;
const { utxos, balanceSats, multisig, bip32Path, spend } = node;
const { expandMode } = this.state;

if (client.type === "public" && expandMode === MODE_WATCH)
Expand All @@ -235,7 +235,10 @@ class AddressExpander extends React.Component {
inputsTotalSats={balanceSats}
multisig={multisig}
bip32Path={bip32Path}
showSelection={false} // need a little more polish before we enable this
hideSelectAllInHeader
selectAll={spend}
node={node}
setSpendCheckbox={setSpendCheckbox}
/>
</Grid>
);
Expand Down Expand Up @@ -418,15 +421,18 @@ AddressExpander.propTypes = {
multisig: PropTypes.shape({
address: PropTypes.string,
}),
spend: PropTypes.bool,
utxos: PropTypes.arrayOf(PropTypes.shape({})),
}).isRequired,
network: PropTypes.string,
requiredSigners: PropTypes.number.isRequired,
totalSigners: PropTypes.number.isRequired,
setSpendCheckbox: PropTypes.func,
};

AddressExpander.defaultProps = {
network: NETWORKS.TESTNET,
setSpendCheckbox: () => {},
};

function mapStateToProps(state) {
Expand All @@ -436,6 +442,7 @@ function mapStateToProps(state) {
totalSigners: state.settings.totalSigners,
client: state.client,
extendedPublicKeyImporters: state.quorum.extendedPublicKeyImporters,
transaction: state.spend.transaction,
};
}

Expand Down
Loading

0 comments on commit ef4ab05

Please sign in to comment.