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

Commit

Permalink
fix(wallet): fixes inconsistencies in wallet balance update after spe…
Browse files Browse the repository at this point in the history
…nd (#131)

* fixes inconsistencies in wallet balance update after spend

* add warning message if utxos don't update

* update formatting of transaction preview totals

* make slice querying after spend more selective

* add error catching for utxo fetch
  • Loading branch information
bucko13 authored May 22, 2020
1 parent 6a8e443 commit fd214eb
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 157 deletions.
134 changes: 100 additions & 34 deletions src/actions/walletActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
setFee,
finalizeOutputs,
} from "./transactionActions";
import { setErrorNotification } from "./errorNotificationActions";
import { getSpendableSlices } from "../selectors/wallet";

export const UPDATE_DEPOSIT_SLICE = "UPDATE_DEPOSIT_SLICE";
Expand Down Expand Up @@ -155,71 +156,136 @@ export function autoSelectCoins() {
};
}

export function spendSlices(inputs, changeSlice) {
/**
* @description Given the state of the transaction store, check status of
* the slices being used in the spend tx and update them once changed state is confirmed.
* @param {Object} changeSlice - the slice that was chosen as the change output
*/
export function updateTxSlices(
changeSlice,
retries = 10,
skipAddresses = new Set()
) {
// eslint-disable-next-line consistent-return
return async (dispatch, getState) => {
const {
settings: { network },
client,
spend: {
transaction: { changeAddress },
transaction: { changeAddress, inputs, txid },
},
wallet: {
deposits: { nextNode },
deposits: { nextNode: nextDepositSlice, nodes: depositSlices },
change: { nodes: changeSlices },
},
} = getState();

const sliceUpdates = [];

// track which slices are already being queried since
// the fetch command will get all utxos for an address.
const addressSet = new Set();
// utility function for getting utxo set of an address
// and formatting the result in a way we can use
const fetchSliceStatus = async (address, bip32Path) => {
const utxos = await fetchAddressUTXOs(address, network, client);
return {
addressUsed: true,
change: isChange(bip32Path),
address,
bip32Path,
...utxos,
};
};

inputs.forEach((input) => {
const { address } = input.multisig;
if (!addressSet.has(address)) {
// array of slices we want to query
const slices = [...inputs];

// if there is a change address in the transaction
// then we want to query the state of that slice as well
if (changeSlice && changeSlice.multisig.address === changeAddress) {
slices.push(changeSlice);
}

// array to store results from queries for each slice
const sliceUpdates = [];

// track which slices are already being queried since
// the fetch command will get all utxos for an address
// and some inputs might be for the same address
const addressSet = new Set();

// go through each slice and see if it needs to be queried
slices.forEach((slice) => {
const { address } = slice.multisig;
// if address isn't already being queried and isn't in the
// set of addresses to skip (which means it's already been
// successfully updated), then fetchSliceStatus
if (!addressSet.has(address) && !skipAddresses.has(address)) {
addressSet.add(address);
// setting up async network calls to await all of them
sliceUpdates.push(fetchSliceStatus(address, input.bip32Path));
sliceUpdates.push(fetchSliceStatus(address, slice.bip32Path));
}
});

// if we have a change slice, then let's query an update for
// that slice too
if (changeSlice && changeSlice.multisig.address === changeAddress) {
addressSet.add(changeAddress);
sliceUpdates.push(fetchSliceStatus(changeAddress, changeSlice.bip32Path));
let queriedSlices;
try {
queriedSlices = await Promise.all(sliceUpdates);
} catch (e) {
return dispatch(
setErrorNotification(
`There was a problem updating wallet state. Try a refresh. Error: ${e.message}`
)
);
}

// check the next deposit slice just in case a spend is to own wallet
// this doesn't catch all self-spend cases but should catch the majority
// to avoid any confusion for less technical users.
sliceUpdates.push(
fetchSliceStatus(nextNode.multisig.address, nextNode.bip32Path)
);
// once all queries have completed we can confirm which have been updated
// and dispatch the changes to the store
for (let i = 0; i < queriedSlices.length; i += 1) {
const slice = queriedSlices[i];
const utxoCount = slice.change
? changeSlices[slice.bip32Path].utxos.length
: depositSlices[slice.bip32Path].utxos.length;

const updatedSlices = await Promise.all(sliceUpdates);

updatedSlices.forEach((slice) => {
if (slice.change)
// if the utxo count is not the same then we can reliably update
// this slice and can skip in any future calls
if (slice && slice.utxos && slice.utxos.length !== utxoCount) {
dispatch({
type: UPDATE_CHANGE_SLICE,
type: slice.change ? UPDATE_CHANGE_SLICE : UPDATE_DEPOSIT_SLICE,
value: slice,
});
else
dispatch({
type: UPDATE_DEPOSIT_SLICE,
value: slice,
});
});
skipAddresses.add(slice.address);
}
}

// if not all slices queried were successful, then we want to recursively call
// updateTxSlices, counting down retries and with the full set of successful queries
// ALL input slices must return a different utxo set otherwise something went wrong
if (skipAddresses.size !== queriedSlices.length && retries)
return setTimeout(
() => dispatch(updateTxSlices(changeSlice, retries - 1, skipAddresses)),
750
);

// if we're out of retries and counts are still the same
// then we're done trying and should show an error
if (!retries) {
let message = `There was a problem updating the wallet balance.
It is recommended you refresh the wallet to make sure UTXO current set is up to date.`;
if (txid && txid.length) message += ` Transaction ID: ${txid}`;
return dispatch(setErrorNotification(message));
}

// Check the next deposit slice just in case a spend is to own wallet.
// This doesn't catch all self-spend cases but should catch the majority
// to avoid any confusion for less technical users.
const updatedSlice = await fetchSliceStatus(
nextDepositSlice.multisig.address,
nextDepositSlice.bip32Path
);

// if its status has changed and the utxo set for the next
// deposit slice is different, then update it as well
if (
updatedSlice.utxos.length !==
depositSlices[updatedSlice.bip32Path].utxos.length
)
return dispatch({ type: UPDATE_DEPOSIT_SLICE, value: updatedSlice });
};
}

Expand Down
7 changes: 4 additions & 3 deletions src/components/Wallet/TransactionPreview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,17 @@ class TransactionPreview extends React.Component {
<Grid container>
<Grid item xs={4}>
<h3>Fee</h3>
<div>{fee}</div>
<div>{BigNumber(fee).toFixed(8)} BTC </div>
</Grid>
<Grid item xs={4}>
<h3>Fee Rate</h3>
<div>{feeRate}</div>
<div>{feeRate} sats/byte</div>
</Grid>
<Grid item xs={4}>
<h3>Total</h3>
<div>
{satoshisToBitcoins(BigNumber(inputsTotalSats || 0)).toString()}
{satoshisToBitcoins(BigNumber(inputsTotalSats || 0)).toFixed(8)}{" "}
BTC
</div>
</Grid>
</Grid>
Expand Down
Loading

0 comments on commit fd214eb

Please sign in to comment.