-
>
)}
>
);
};
export default SendToken;
diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js
index f02ca752e..28efaa499 100644
--- a/web/cashtab/src/hooks/useBCH.js
+++ b/web/cashtab/src/hooks/useBCH.js
@@ -1,705 +1,535 @@
import BigNumber from 'bignumber.js';
import { currency } from '../components/Common/Ticker';
export default function useBCH() {
const DUST = 0.000005;
const SEND_BCH_ERRORS = {
INSUFICIENT_FUNDS: 0,
NETWORK_ERROR: 1,
INSUFFICIENT_PRIORITY: 66, // ~insufficient fee
DOUBLE_SPENDING: 18,
MAX_UNCONFIRMED_TXS: 64,
};
const getRestUrl = (apiIndex = 0) => {
const apiString =
process.env.REACT_APP_NETWORK === `mainnet`
? process.env.REACT_APP_BCHA_APIS
: process.env.REACT_APP_BCHA_APIS_TEST;
const apiArray = apiString.split(',');
return apiArray[apiIndex];
};
const getTxHistory = async (BCH, addresses) => {
let txHistoryResponse;
try {
//console.log(`API Call: BCH.Electrumx.utxo(addresses)`);
//console.log(addresses);
txHistoryResponse = await BCH.Electrumx.transactions(addresses);
//console.log(`BCH.Electrumx.transactions(addresses) succeeded`);
//console.log(`txHistoryResponse`, txHistoryResponse);
if (txHistoryResponse.success && txHistoryResponse.transactions) {
return txHistoryResponse.transactions;
} else {
// eslint-disable-next-line no-throw-literal
throw new Error('Error in getTxHistory');
}
} catch (err) {
console.log(`Error in BCH.Electrumx.transactions(addresses):`);
console.log(err);
return err;
}
};
// Split out the BCH.Electrumx.utxo(addresses) call from the getSlpBalancesandUtxos function
// If utxo set has not changed, you do not need to hydrate the utxo set
// This drastically reduces calls to the API
const getUtxos = async (BCH, addresses) => {
let utxosResponse;
try {
//console.log(`API Call: BCH.Electrumx.utxo(addresses)`);
//console.log(addresses);
utxosResponse = await BCH.Electrumx.utxo(addresses);
//console.log(`BCH.Electrumx.utxo(addresses) succeeded`);
//console.log(`utxosResponse`, utxosResponse);
return utxosResponse.utxos;
} catch (err) {
console.log(`Error in BCH.Electrumx.utxo(addresses):`);
return err;
}
};
const getSlpBalancesAndUtxos = async (BCH, utxos) => {
let hydratedUtxoDetails;
try {
hydratedUtxoDetails = await BCH.SLP.Utils.hydrateUtxos(utxos);
//console.log(`hydratedUtxoDetails`, hydratedUtxoDetails);
} catch (err) {
console.log(
`Error in BCH.SLP.Utils.hydrateUtxos(utxosResponse.utxos)`,
);
console.log(err);
}
const hydratedUtxos = [];
for (let i = 0; i < hydratedUtxoDetails.slpUtxos.length; i += 1) {
const hydratedUtxosAtAddress = hydratedUtxoDetails.slpUtxos[i];
for (let j = 0; j < hydratedUtxosAtAddress.utxos.length; j += 1) {
const hydratedUtxo = hydratedUtxosAtAddress.utxos[j];
hydratedUtxo.address = hydratedUtxosAtAddress.address;
hydratedUtxos.push(hydratedUtxo);
}
}
//console.log(`hydratedUtxos`, hydratedUtxos);
// WARNING
// If you hit rate limits, your above utxos object will come back with `isValid` as null, but otherwise ok
// You need to throw an error before setting nonSlpUtxos and slpUtxos in this case
const nullUtxos = hydratedUtxos.filter(utxo => utxo.isValid === null);
//console.log(`nullUtxos`, nullUtxos);
if (nullUtxos.length > 0) {
console.log(
`${nullUtxos.length} null utxos found, ignoring results`,
);
throw new Error('Null utxos found, ignoring results');
}
// Prevent app from treating slpUtxos as nonSlpUtxos
// Must enforce === false as api will occasionally return utxo.isValid === null
// Do not classify utxos with 546 satoshis as nonSlpUtxos as a precaution
// Do not classify any utxos that include token information as nonSlpUtxos
const nonSlpUtxos = hydratedUtxos.filter(
utxo =>
utxo.isValid === false &&
utxo.satoshis !== 546 &&
!utxo.tokenName,
);
const slpUtxos = hydratedUtxos.filter(utxo => utxo.isValid);
let tokensById = {};
slpUtxos.forEach(slpUtxo => {
let token = tokensById[slpUtxo.tokenId];
if (token) {
// Minting baton does nto have a slpUtxo.tokenQty type
if (slpUtxo.tokenQty) {
token.balance = token.balance.plus(
new BigNumber(slpUtxo.tokenQty),
);
}
//token.hasBaton = slpUtxo.transactionType === "genesis";
if (slpUtxo.utxoType && !token.hasBaton) {
token.hasBaton = slpUtxo.utxoType === 'minting-baton';
}
// Examples of slpUtxo
/*
- Genesis transaction:
- {
- address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6"
- decimals: 9
- height: 617564
- isValid: true
- satoshis: 546
- tokenDocumentHash: ""
- tokenDocumentUrl: "developer.bitcoin.com"
- tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
- tokenName: "PiticoLaunch"
- tokenTicker: "PTCL"
- tokenType: 1
- tx_hash: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
- tx_pos: 2
- txid: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
- utxoType: "minting-baton"
- value: 546
- vout: 2
- }
+ Genesis transaction:
+ {
+ address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6"
+ decimals: 9
+ height: 617564
+ isValid: true
+ satoshis: 546
+ tokenDocumentHash: ""
+ tokenDocumentUrl: "developer.bitcoin.com"
+ tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
+ tokenName: "PiticoLaunch"
+ tokenTicker: "PTCL"
+ tokenType: 1
+ tx_hash: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
+ tx_pos: 2
+ txid: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
+ utxoType: "minting-baton"
+ value: 546
+ vout: 2
+ }
- Send transaction:
- {
- address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6"
- decimals: 9
- height: 655115
- isValid: true
- satoshis: 546
- tokenDocumentHash: ""
- tokenDocumentUrl: "developer.bitcoin.com"
- tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
- tokenName: "PiticoLaunch"
- tokenQty: 1.123456789
- tokenTicker: "PTCL"
- tokenType: 1
- transactionType: "send"
- tx_hash: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e"
- tx_pos: 1
- txid: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e"
- utxoType: "token"
- value: 546
- vout: 1
- }
- */
+ Send transaction:
+ {
+ address: "bitcoincash:qrhzv5t79e2afc3rdutcu0d3q20gl7ul3ue58whah6"
+ decimals: 9
+ height: 655115
+ isValid: true
+ satoshis: 546
+ tokenDocumentHash: ""
+ tokenDocumentUrl: "developer.bitcoin.com"
+ tokenId: "6c41f244676ecfcbe3b4fabee2c72c2dadf8d74f8849afabc8a549157db69199"
+ tokenName: "PiticoLaunch"
+ tokenQty: 1.123456789
+ tokenTicker: "PTCL"
+ tokenType: 1
+ transactionType: "send"
+ tx_hash: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e"
+ tx_pos: 1
+ txid: "dea400f963bc9f51e010f88533010f8d1f82fc2bcc485ff8500c3a82b25abd9e"
+ utxoType: "token"
+ value: 546
+ vout: 1
+ }
+ */
} else {
token = {};
token.info = slpUtxo;
token.tokenId = slpUtxo.tokenId;
if (slpUtxo.tokenQty) {
token.balance = new BigNumber(slpUtxo.tokenQty);
} else {
token.balance = new BigNumber(0);
}
if (slpUtxo.utxoType) {
token.hasBaton = slpUtxo.utxoType === 'minting-baton';
} else {
token.hasBaton = false;
}
tokensById[slpUtxo.tokenId] = token;
}
});
const tokens = Object.values(tokensById);
// console.log(`tokens`, tokens);
return {
tokens,
nonSlpUtxos,
slpUtxos,
};
};
const calcFee = (
BCH,
utxos,
p2pkhOutputNumber = 2,
satoshisPerByte = currency.defaultFee,
) => {
const byteCount = BCH.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: p2pkhOutputNumber },
);
const txFee = Math.ceil(satoshisPerByte * byteCount);
return txFee;
};
const sendToken = async (
BCH,
wallet,
slpBalancesAndUtxos,
{ tokenId, amount, tokenReceiverAddress },
) => {
+ // Handle error of user having no BCH
+ if (slpBalancesAndUtxos.nonSlpUtxos.length === 0) {
+ throw new Error(
+ `You need some ${currency.ticker} to send ${currency.tokenTicker}`,
+ );
+ }
const largestBchUtxo = slpBalancesAndUtxos.nonSlpUtxos.reduce(
(previous, current) =>
previous.satoshis > current.satoshis ? previous : current,
);
- // console.log(`largestBchUtxo`, largestBchUtxo);
- // this is big enough? might need to combine utxos
- // TODO improve utxo selection
- /*
- {
- address: "bitcoincash:qrcl220pxeec78vnchwyh6fsdyf60uv9tcynw3u2ev"
- height: 0
- isValid: false
- satoshis: 1510
- tx_hash: "faef4d8bf56353702e29c22f2aace970ddbac617144456d509e23e1192b320a8"
- tx_pos: 0
- txid: "faef4d8bf56353702e29c22f2aace970ddbac617144456d509e23e1192b320a8"
- value: 1510
- vout: 0
- wif: "removed for git potential"
- }
- */
+
const bchECPair = BCH.ECPair.fromWIF(largestBchUtxo.wif);
const tokenUtxos = slpBalancesAndUtxos.slpUtxos.filter(
(utxo, index) => {
if (
utxo && // UTXO is associated with a token.
utxo.tokenId === tokenId && // UTXO matches the token ID.
utxo.utxoType === 'token' // UTXO is not a minting baton.
) {
return true;
}
return false;
},
);
if (tokenUtxos.length === 0) {
throw new Error(
'No token UTXOs for the specified token could be found.',
);
}
// BEGIN transaction construction.
// instance of transaction builder
let transactionBuilder;
if (process.env.REACT_APP_NETWORK === 'mainnet') {
transactionBuilder = new BCH.TransactionBuilder();
} else transactionBuilder = new BCH.TransactionBuilder('testnet');
const originalAmount = largestBchUtxo.value;
transactionBuilder.addInput(
largestBchUtxo.tx_hash,
largestBchUtxo.tx_pos,
);
let finalTokenAmountSent = new BigNumber(0);
let tokenAmountBeingSentToAddress = new BigNumber(amount);
- /*
- console.log(`tokenAmountBeingSentToAddress`, tokenAmountBeingSentToAddress);
- console.log(
- `tokenAmountBeingSentToAddress.toString()`,
- tokenAmountBeingSentToAddress.toString()
- );
- */
+
let tokenUtxosBeingSpent = [];
for (let i = 0; i < tokenUtxos.length; i++) {
finalTokenAmountSent = finalTokenAmountSent.plus(
- new BigNumber(tokenUtxos[i].tokenQty).div(
- Math.pow(10, tokenUtxos[i].decimals),
- ),
+ new BigNumber(tokenUtxos[i].tokenQty),
);
transactionBuilder.addInput(
tokenUtxos[i].tx_hash,
tokenUtxos[i].tx_pos,
);
tokenUtxosBeingSpent.push(tokenUtxos[i]);
if (tokenAmountBeingSentToAddress.lte(finalTokenAmountSent)) {
break;
}
}
- // Run a test function to mock the outputs generated by BCH.SLP.TokenType1.generateSendOpReturn below
- slpDebug(
- tokenUtxosBeingSpent,
- tokenAmountBeingSentToAddress.toString(),
- );
-
- // Generate the OP_RETURN code.
- console.log(`Debug output`);
- console.log(`tokenUtxos`, tokenUtxosBeingSpent);
- console.log(`sendQty`, tokenAmountBeingSentToAddress.toString());
const slpSendObj = BCH.SLP.TokenType1.generateSendOpReturn(
tokenUtxosBeingSpent,
tokenAmountBeingSentToAddress.toString(),
);
const slpData = slpSendObj.script;
// Add OP_RETURN as first output.
transactionBuilder.addOutput(slpData, 0);
// Send dust transaction representing tokens being sent.
transactionBuilder.addOutput(
BCH.SLP.Address.toLegacyAddress(tokenReceiverAddress),
546,
);
// Return any token change back to the sender.
if (slpSendObj.outputs > 1) {
transactionBuilder.addOutput(
BCH.SLP.Address.toLegacyAddress(
tokenUtxosBeingSpent[0].address,
),
546,
);
}
// get byte count to calculate fee. paying 1 sat
// Note: This may not be totally accurate. Just guessing on the byteCount size.
const txFee = calcFee(
BCH,
tokenUtxosBeingSpent,
5,
1.1 * currency.defaultFee,
);
// amount to send back to the sending address. It's the original amount - 1 sat/byte for tx size
const remainder = originalAmount - txFee - 546 * 2;
if (remainder < 1) {
throw new Error('Selected UTXO does not have enough satoshis');
}
// Last output: send the BCH change back to the wallet.
transactionBuilder.addOutput(
BCH.Address.toLegacyAddress(largestBchUtxo.address),
remainder,
);
// Sign the transaction with the private key for the BCH UTXO paying the fees.
let redeemScript;
transactionBuilder.sign(
0,
bchECPair,
redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL,
originalAmount,
);
// Sign each token UTXO being consumed.
for (let i = 0; i < tokenUtxosBeingSpent.length; i++) {
const thisUtxo = tokenUtxosBeingSpent[i];
const accounts = [wallet.Path245, wallet.Path145];
const utxoEcPair = BCH.ECPair.fromWIF(
accounts
.filter(acc => acc.cashAddress === thisUtxo.address)
.pop().fundingWif,
);
transactionBuilder.sign(
1 + i,
utxoEcPair,
redeemScript,
transactionBuilder.hashTypes.SIGHASH_ALL,
thisUtxo.value,
);
}
// build tx
const tx = transactionBuilder.build();
// output rawhex
const hex = tx.toHex();
// console.log(`Transaction raw hex: `, hex);
// END transaction construction.
- // Broadcast transaction to the network
-
const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]);
if (txidStr && txidStr[0]) {
console.log(`${currency.tokenTicker} txid`, txidStr[0]);
}
let link;
if (process.env.REACT_APP_NETWORK === `mainnet`) {
link = `${currency.blockExplorerUrl}/tx/${txidStr}`;
} else {
link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`;
}
+
//console.log(`link`, link);
return link;
};
- const slpDebug = (tokenUtxos, sendQty) => {
- console.log(`slpDebug test called with`);
- console.log(`tokenUtxos`, tokenUtxos);
- console.log(`sendQty`, sendQty);
- try {
- //const tokenId = tokenUtxos[0].tokenId;
- const decimals = tokenUtxos[0].decimals;
-
- // Joey patch to do
- // totalTokens must be a big number accounting for decimals
- // sendQty must be the same
- /* From slp-sdk
-
- amount = new BigNumber(amount).times(10 ** tokenDecimals) // Don't forget to account for token precision
-
-
- This is analagous to sendQty here
- */
- const sendQtyBig = new BigNumber(sendQty).times(10 ** decimals);
-
- // Calculate the total amount of tokens owned by the wallet.
- //let totalTokens = 0;
- //for (let i = 0; i < tokenUtxos.length; i++) totalTokens += tokenUtxos[i].tokenQty;
-
- // Calculate total amount of tokens using Big Number throughout
- /*
- let totalTokens = new BigNumber(0);
- for (let i = 0; i < tokenUtxos.length; i++) {
- console.log(`tokenQty normal`, tokenUtxos[i].tokenQty);
- const thisTokenQty = new BigNumber(tokenUtxos[i].tokenQty);
- totalTokens.plus(thisTokenQty);
- }
- totalTokens.times(10 ** decimals);
- */
- let totalTokens = tokenUtxos.reduce((tot, txo) => {
- return tot.plus(
- new BigNumber(txo.tokenQty).times(10 ** decimals),
- );
- }, new BigNumber(0));
-
- console.log(`totalTokens`, totalTokens);
- //test
- //totalTokens = new BigNumber(totalTokens).times(10 ** decimals);
-
- console.log(`sendQtyBig`, sendQtyBig);
- const change = totalTokens.minus(sendQtyBig);
- console.log(`change`, change);
-
- //let script;
- //let outputs = 1;
-
- // The normal case, when there is token change to return to sender.
- if (change > 0) {
- //outputs = 2;
-
- // Convert the send quantity to the format expected by slp-mdm.
-
- //let baseQty = new BigNumber(sendQty).times(10 ** decimals);
- // Update: you've done this earlier, so don't do it now
- let baseQty = sendQtyBig.toString();
- console.log(`baseQty: `, baseQty);
-
- // Convert the change quantity to the format expected by slp-mdm.
- //let baseChange = new BigNumber(change).times(10 ** decimals);
- // Update: you've done this earlier, so don't do it now
- let baseChange = change.toString();
- console.log(`baseChange: `, baseChange);
-
- const outputQty = new BigNumber(baseChange).plus(
- new BigNumber(baseQty),
- );
- const inputQty = new BigNumber(totalTokens);
- console.log(
- `new BigNumber(baseChange)`,
- new BigNumber(baseChange),
- );
- console.log(`new BigNumber(baseQty)`, new BigNumber(baseQty));
- console.log(`outputQty:`, outputQty);
- console.log(`inputQty:`, inputQty);
- console.log(
- `outputQty.minus(inputQty).toString():`,
- outputQty.minus(inputQty).toString(),
- );
- console.log(
- `outputQty.minus(inputQty).toString():`,
- outputQty.minus(inputQty).toString() === '0',
- );
-
- const tokenOutputDelta =
- outputQty.minus(inputQty).toString() !== '0';
- if (tokenOutputDelta)
- console.log(
- 'Token transaction inputs do not match outputs, cannot send transaction',
- );
- // Generate the OP_RETURN as a Buffer.
- /*
- script = slpMdm.TokenType1.send(tokenId, [
- new slpMdm.BN(baseQty),
- new slpMdm.BN(baseChange)
- ]);
- */
- //
-
- // Corner case, when there is no token change to send back.
- } else {
- console.log(`No change case:`);
- let baseQty = sendQtyBig.toString();
- console.log(`baseQty: `, baseQty);
-
- // Check for potential burns
- const noChangeOutputQty = new BigNumber(baseQty);
- const noChangeInputQty = new BigNumber(totalTokens);
- console.log(`noChangeOutputQty`, noChangeOutputQty);
- console.log(`noChangeInputQty`, noChangeInputQty);
-
- const tokenSingleOutputError =
- noChangeOutputQty.minus(noChangeInputQty).toString() !==
- '0';
- if (tokenSingleOutputError)
- console.log(
- 'Token transaction inputs do not match outputs, cannot send transaction',
- );
-
- // Generate the OP_RETURN as a Buffer.
- //script = slpMdm.TokenType1.send(tokenId, [new slpMdm.BN(baseQty)]);
- }
- } catch (err) {
- console.log(`Error in generateSendOpReturn()`);
- throw err;
- }
- };
-
const sendBch = async (
BCH,
wallet,
utxos,
{ addresses, values, encodedOpReturn },
callbackTxId,
) => {
// Note: callbackTxId is a callback function that accepts a txid as its only parameter
- /* Debug logs
- console.log(`sendBch called with`);
- console.log("BCH", BCH);
- console.log("wallet", wallet);
- console.log("utxos", utxos);
- console.log("addresses", addresses);
- console.log("values", values);
- console.log("encodedOpReturn", encodedOpReturn);
- console.log("callbackTxid", callbackTxId);
- */
+
try {
if (!values || values.length === 0) {
return null;
}
const value = values.reduce(
(previous, current) => new BigNumber(current).plus(previous),
new BigNumber(0),
);
const REMAINDER_ADDR = wallet.Path145.cashAddress;
const inputUtxos = [];
let transactionBuilder;
// instance of transaction builder
if (process.env.REACT_APP_NETWORK === `mainnet`)
transactionBuilder = new BCH.TransactionBuilder();
else transactionBuilder = new BCH.TransactionBuilder('testnet');
const satoshisToSend = BCH.BitcoinCash.toSatoshi(value.toFixed(8));
let originalAmount = new BigNumber(0);
let txFee = 0;
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i];
originalAmount = originalAmount.plus(utxo.satoshis);
const vout = utxo.vout;
const txid = utxo.txid;
// add input with txid and index of vout
transactionBuilder.addInput(txid, vout);
inputUtxos.push(utxo);
txFee = encodedOpReturn
? calcFee(BCH, inputUtxos, addresses.length + 2)
: calcFee(BCH, inputUtxos, addresses.length + 1);
if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) {
break;
}
}
// amount to send back to the remainder address.
const remainder = Math.floor(
originalAmount.minus(satoshisToSend).minus(txFee),
);
if (remainder < 0) {
const error = new Error(`Insufficient funds`);
error.code = SEND_BCH_ERRORS.INSUFICIENT_FUNDS;
throw error;
}
if (encodedOpReturn) {
transactionBuilder.addOutput(encodedOpReturn, 0);
}
// add output w/ address and amount to send
for (let i = 0; i < addresses.length; i++) {
const address = addresses[i];
transactionBuilder.addOutput(
BCH.Address.toCashAddress(address),
BCH.BitcoinCash.toSatoshi(Number(values[i]).toFixed(8)),
);
}
if (remainder >= BCH.BitcoinCash.toSatoshi(DUST)) {
transactionBuilder.addOutput(REMAINDER_ADDR, remainder);
}
// Sign the transactions with the HD node.
for (let i = 0; i < inputUtxos.length; i++) {
const utxo = inputUtxos[i];
transactionBuilder.sign(
i,
BCH.ECPair.fromWIF(utxo.wif),
undefined,
transactionBuilder.hashTypes.SIGHASH_ALL,
utxo.satoshis,
);
}
// build tx
const tx = transactionBuilder.build();
// output rawhex
const hex = tx.toHex();
// Broadcast transaction to the network
const txidStr = await BCH.RawTransactions.sendRawTransaction([hex]);
if (txidStr && txidStr[0]) {
console.log(`${currency.ticker} txid`, txidStr[0]);
}
let link;
if (callbackTxId) {
callbackTxId(txidStr);
}
if (process.env.REACT_APP_NETWORK === `mainnet`) {
link = `${currency.blockExplorerUrl}/tx/${txidStr}`;
} else {
link = `${currency.blockExplorerUrlTestnet}/tx/${txidStr}`;
}
//console.log(`link`, link);
return link;
} catch (err) {
if (err.error === 'insufficient priority (code 66)') {
err.code = SEND_BCH_ERRORS.INSUFFICIENT_PRIORITY;
} else if (err.error === 'txn-mempool-conflict (code 18)') {
err.code = SEND_BCH_ERRORS.DOUBLE_SPENDING;
} else if (err.error === 'Network Error') {
err.code = SEND_BCH_ERRORS.NETWORK_ERROR;
} else if (
err.error ===
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)'
) {
err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS;
}
console.log(`error: `, err);
throw err;
}
};
const getBCH = (apiIndex = 0, fromWindowObject = true) => {
if (fromWindowObject && window.SlpWallet) {
const SlpWallet = new window.SlpWallet('', {
restURL: getRestUrl(apiIndex),
});
return SlpWallet.bchjs;
}
};
return {
getBCH,
calcFee,
getUtxos,
getSlpBalancesAndUtxos,
getTxHistory,
getRestUrl,
sendBch,
sendToken,
};
}
diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js
index 8ec125dac..80391cb28 100644
--- a/web/cashtab/src/hooks/useWallet.js
+++ b/web/cashtab/src/hooks/useWallet.js
@@ -1,1099 +1,1104 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import Paragraph from 'antd/lib/typography/Paragraph';
import { notification } from 'antd';
import useAsyncTimeout from './useAsyncTimeout';
import usePrevious from './usePrevious';
import useBCH from '../hooks/useBCH';
import BigNumber from 'bignumber.js';
import localforage from 'localforage';
import { currency } from '../components/Common/Ticker';
import _ from 'lodash';
const useWallet = () => {
const [wallet, setWallet] = useState(false);
const [fiatPrice, setFiatPrice] = useState(null);
const [ws, setWs] = useState(null);
const [apiError, setApiError] = useState(false);
const [walletState, setWalletState] = useState({
balances: {},
tokens: [],
slpBalancesAndUtxos: [],
txHistory: [],
});
const { getBCH, getUtxos, getSlpBalancesAndUtxos, getTxHistory } = useBCH();
const [loading, setLoading] = useState(true);
const [apiIndex, setApiIndex] = useState(0);
const [BCH, setBCH] = useState(getBCH(apiIndex));
const [utxos, setUtxos] = useState(null);
const { balances, tokens, slpBalancesAndUtxos, txHistory } = walletState;
const previousBalances = usePrevious(balances);
const previousTokens = usePrevious(tokens);
const previousWallet = usePrevious(wallet);
const previousUtxos = usePrevious(utxos);
// If you catch API errors, call this function
const tryNextAPI = () => {
let currentApiIndex = apiIndex;
// How many APIs do you have?
const apiString = process.env.REACT_APP_BCHA_APIS;
const apiArray = apiString.split(',');
console.log(`You have ${apiArray.length} APIs to choose from`);
console.log(`Current selection: ${apiIndex}`);
// If only one, exit
if (apiArray.length === 0) {
console.log(
`There are no backup APIs, you are stuck with this error`,
);
return;
} else if (currentApiIndex < apiArray.length - 1) {
currentApiIndex += 1;
console.log(
`Incrementing API index from ${apiIndex} to ${currentApiIndex}`,
);
} else {
// Otherwise use the first option again
console.log(`Retrying first API index`);
currentApiIndex = 0;
}
//return setApiIndex(currentApiIndex);
console.log(`Setting Api Index to ${currentApiIndex}`);
setApiIndex(currentApiIndex);
return setBCH(getBCH(currentApiIndex));
// If you have more than one, use the next one
// If you are at the "end" of the array, use the first one
};
const normalizeSlpBalancesAndUtxos = (slpBalancesAndUtxos, wallet) => {
const Accounts = [wallet.Path245, wallet.Path145];
slpBalancesAndUtxos.nonSlpUtxos.forEach(utxo => {
const derivatedAccount = Accounts.find(
account => account.cashAddress === utxo.address,
);
utxo.wif = derivatedAccount.fundingWif;
});
return slpBalancesAndUtxos;
};
const normalizeBalance = slpBalancesAndUtxos => {
const totalBalanceInSatoshis = slpBalancesAndUtxos.nonSlpUtxos.reduce(
(previousBalance, utxo) => previousBalance + utxo.satoshis,
0,
);
return {
totalBalanceInSatoshis,
totalBalance: BCH.BitcoinCash.toBitcoinCash(totalBalanceInSatoshis),
};
};
const deriveAccount = async ({ masterHDNode, path }) => {
const node = BCH.HDNode.derivePath(masterHDNode, path);
const cashAddress = BCH.HDNode.toCashAddress(node);
const slpAddress = BCH.SLP.Address.toSLPAddress(cashAddress);
return {
cashAddress,
slpAddress,
fundingWif: BCH.HDNode.toWIF(node),
fundingAddress: BCH.SLP.Address.toSLPAddress(cashAddress),
legacyAddress: BCH.SLP.Address.toLegacyAddress(cashAddress),
};
};
const haveUtxosChanged = (utxos, previousUtxos) => {
// Relevant points for this array comparing exercise
// https://stackoverflow.com/questions/13757109/triple-equal-signs-return-false-for-arrays-in-javascript-why
// https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript
// If this is initial state
if (utxos === null) {
// Then make sure to get slpBalancesAndUtxos
return true;
}
// If this is the first time the wallet received utxos
if (
typeof previousUtxos === 'undefined' ||
typeof utxos === 'undefined'
) {
// Then they have certainly changed
return true;
}
// return true for empty array, since this means you definitely do not want to skip the next API call
if (utxos && utxos.length === 0) {
return true;
}
// Compare utxo sets
const utxoArraysUnchanged = _.isEqual(utxos, previousUtxos);
// If utxos are not the same as previousUtxos
if (utxoArraysUnchanged) {
// then utxos have not changed
return false;
// otherwise,
} else {
// utxos have changed
return true;
}
};
const update = async ({ wallet, setWalletState }) => {
//console.log(`tick()`);
//console.time("update");
try {
if (!wallet) {
return;
}
const cashAddresses = [
wallet.Path245.cashAddress,
wallet.Path145.cashAddress,
];
const utxos = await getUtxos(BCH, cashAddresses);
//console.log(`utxos`, utxos);
// If an error is returned or utxos from only 1 address are returned
if (!utxos || _.isEmpty(utxos) || utxos.error || utxos.length < 2) {
// Throw error here to prevent more attempted api calls
// as you are likely already at rate limits
throw new Error('Error fetching utxos');
}
setUtxos(utxos);
const utxosHaveChanged = haveUtxosChanged(utxos, previousUtxos);
// If the utxo set has not changed,
if (!utxosHaveChanged) {
// remove api error here; otherwise it will remain if recovering from a rate
// limit error with an unchanged utxo set
setApiError(false);
// then walletState has not changed and does not need to be updated
//console.timeEnd("update");
return;
}
// todo: another available optimization, update slpBalancesandUtxos by hydrating only the new utxos
const slpBalancesAndUtxos = await getSlpBalancesAndUtxos(
BCH,
utxos,
);
const txHistory = await getTxHistory(BCH, cashAddresses);
console.log(`slpBalancesAndUtxos`, slpBalancesAndUtxos);
if (typeof slpBalancesAndUtxos === 'undefined') {
console.log(`slpBalancesAndUtxos is undefined`);
throw new Error('slpBalancesAndUtxos is undefined');
}
const { tokens } = slpBalancesAndUtxos;
const newState = {
balances: {},
tokens: [],
slpBalancesAndUtxos: [],
};
newState.slpBalancesAndUtxos = normalizeSlpBalancesAndUtxos(
slpBalancesAndUtxos,
wallet,
);
newState.balances = normalizeBalance(slpBalancesAndUtxos);
newState.tokens = tokens;
newState.txHistory = txHistory;
setWalletState(newState);
// If everything executed correctly, remove apiError
setApiError(false);
} catch (error) {
console.log(`Error in update({wallet, setWalletState})`);
console.log(error);
// Set this in state so that transactions are disabled until the issue is resolved
setApiError(true);
//console.timeEnd("update");
// Try another endpoint
console.log(`Trying next API...`);
tryNextAPI();
}
//console.timeEnd("update");
};
const getWallet = async () => {
let wallet;
try {
let existingWallet;
try {
existingWallet = await localforage.getItem('wallet');
// If not in localforage then existingWallet = false, check localstorage
if (!existingWallet) {
console.log(`no existing wallet, checking local storage`);
existingWallet = JSON.parse(
window.localStorage.getItem('wallet'),
);
console.log(
`existingWallet from localStorage`,
existingWallet,
);
// If you find it here, move it to indexedDb
if (existingWallet !== null) {
wallet = await getWalletDetails(existingWallet);
await localforage.setItem('wallet', wallet);
return wallet;
}
}
} catch (e) {
console.log(e);
existingWallet = null;
}
// If no wallet in indexedDb or localforage or caught error above or the initial 'false' is in indexedDB
if (existingWallet === null || !existingWallet) {
wallet = await getWalletDetails(existingWallet);
await localforage.setItem('wallet', wallet);
} else {
wallet = existingWallet;
}
// todo: only do this if you didn't get it out of storage
//wallet = await getWalletDetails(existingWallet);
//await localforage.setItem("wallet", wallet);
} catch (error) {
console.log(error);
}
return wallet;
};
const getWalletDetails = async wallet => {
if (!wallet) {
return false;
}
// Since this info is in localforage now, only get the var
const NETWORK = process.env.REACT_APP_NETWORK;
const mnemonic = wallet.mnemonic;
const rootSeedBuffer = await BCH.Mnemonic.toSeed(mnemonic);
let masterHDNode;
if (NETWORK === `mainnet`)
masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer);
else masterHDNode = BCH.HDNode.fromSeed(rootSeedBuffer, 'testnet');
const Path245 = await deriveAccount({
masterHDNode,
path: "m/44'/245'/0'/0/0",
});
const Path145 = await deriveAccount({
masterHDNode,
path: "m/44'/145'/0'/0/0",
});
let name = Path145.cashAddress.slice(12, 17);
// Only set the name if it does not currently exist
if (wallet && wallet.name) {
name = wallet.name;
}
return {
mnemonic: wallet.mnemonic,
name,
Path245,
Path145,
};
};
const getSavedWallets = async activeWallet => {
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
if (savedWallets === null) {
savedWallets = [];
}
} catch (err) {
console.log(`Error in getSavedWallets`);
console.log(err);
savedWallets = [];
}
// Even though the active wallet is still stored in savedWallets, don't return it in this function
for (let i = 0; i < savedWallets.length; i += 1) {
if (
typeof activeWallet !== 'undefined' &&
activeWallet.name &&
savedWallets[i].name === activeWallet.name
) {
savedWallets.splice(i, 1);
}
}
return savedWallets;
};
const activateWallet = async walletToActivate => {
/*
If the user is migrating from old version to this version, make sure to save the activeWallet
1 - check savedWallets for the previously active wallet
2 - If not there, add it
*/
let currentlyActiveWallet;
try {
currentlyActiveWallet = await localforage.getItem('wallet');
} catch (err) {
console.log(
`Error in localforage.getItem("wallet") in activateWallet()`,
);
return false;
}
// Get savedwallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in localforage.getItem("savedWallets") in activateWallet()`,
);
return false;
}
// Check savedWallets for currentlyActiveWallet
let walletInSavedWallets = false;
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === currentlyActiveWallet.name) {
walletInSavedWallets = true;
}
}
if (!walletInSavedWallets) {
console.log(`Wallet is not in saved Wallets, adding`);
savedWallets.push(currentlyActiveWallet);
// resave savedWallets
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets") in activateWallet()`,
);
}
}
// Now that we have verified the last wallet was saved, we can activate the new wallet
try {
await localforage.setItem('wallet', walletToActivate);
} catch (err) {
console.log(
`Error in localforage.setItem("wallet", walletToActivate) in activateWallet()`,
);
return false;
}
return walletToActivate;
};
const renameWallet = async (oldName, newName) => {
// Load savedWallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in await localforage.getItem("savedWallets") in renameWallet`,
);
console.log(err);
return false;
}
// Verify that no existing wallet has this name
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === newName) {
// return an error
return false;
}
}
// change name of desired wallet
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === oldName) {
// Replace the name of this entry with the new name
savedWallets[i].name = newName;
}
}
// resave savedWallets
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets", savedWallets) in renameWallet()`,
);
return false;
}
return true;
};
const deleteWallet = async walletToBeDeleted => {
// delete a wallet
// returns true if wallet is successfully deleted
// otherwise returns false
// Load savedWallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
} catch (err) {
console.log(
`Error in await localforage.getItem("savedWallets") in deleteWallet`,
);
console.log(err);
return false;
}
// Iterate over to find the wallet to be deleted
// Verify that no existing wallet has this name
let walletFoundAndRemoved = false;
for (let i = 0; i < savedWallets.length; i += 1) {
if (savedWallets[i].name === walletToBeDeleted.name) {
// Verify it has the same mnemonic too, that's a better UUID
if (savedWallets[i].mnemonic === walletToBeDeleted.mnemonic) {
// Delete it
savedWallets.splice(i, 1);
walletFoundAndRemoved = true;
}
}
}
// If you don't find the wallet, return false
if (!walletFoundAndRemoved) {
return false;
}
// Resave savedWallets less the deleted wallet
try {
// Set walletName as the active wallet
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets", savedWallets) in deleteWallet()`,
);
return false;
}
return true;
};
const addNewSavedWallet = async importMnemonic => {
// Add a new wallet to savedWallets from importMnemonic or just new wallet
const lang = 'english';
// create 128 bit BIP39 mnemonic
const Bip39128BitMnemonic = importMnemonic
? importMnemonic
: BCH.Mnemonic.generate(128, BCH.Mnemonic.wordLists()[lang]);
const newSavedWallet = await getWalletDetails({
mnemonic: Bip39128BitMnemonic.toString(),
});
// Get saved wallets
let savedWallets;
try {
savedWallets = await localforage.getItem('savedWallets');
// If this doesn't exist yet, savedWallets === null
if (savedWallets === null) {
savedWallets = [];
}
} catch (err) {
console.log(
`Error in savedWallets = await localforage.getItem("savedWallets") in addNewSavedWallet()`,
);
console.log(err);
console.log(`savedWallets in error state`, savedWallets);
}
// If this wallet is from an imported mnemonic, make sure it does not already exist in savedWallets
if (importMnemonic) {
for (let i = 0; i < savedWallets.length; i += 1) {
// Check for condition "importing new wallet that is already in savedWallets"
if (savedWallets[i].mnemonic === importMnemonic) {
// set this as the active wallet to keep name history
console.log(
`Error: this wallet already exists in savedWallets`,
);
console.log(`Wallet not being added.`);
return false;
}
}
}
// add newSavedWallet
savedWallets.push(newSavedWallet);
// update savedWallets
try {
await localforage.setItem('savedWallets', savedWallets);
} catch (err) {
console.log(
`Error in localforage.setItem("savedWallets", activeWallet) called in createWallet with ${importMnemonic}`,
);
console.log(`savedWallets`, savedWallets);
console.log(err);
}
return true;
};
const createWallet = async importMnemonic => {
const lang = 'english';
// create 128 bit BIP39 mnemonic
const Bip39128BitMnemonic = importMnemonic
? importMnemonic
: BCH.Mnemonic.generate(128, BCH.Mnemonic.wordLists()[lang]);
const wallet = await getWalletDetails({
mnemonic: Bip39128BitMnemonic.toString(),
});
try {
await localforage.setItem('wallet', wallet);
} catch (err) {
console.log(
`Error setting wallet to wallet indexedDb in createWallet()`,
);
console.log(err);
}
// Since this function is only called from OnBoarding.js, also add this to the saved wallet
try {
await localforage.setItem('savedWallets', [wallet]);
} catch (err) {
console.log(
`Error setting wallet to savedWallets indexedDb in createWallet()`,
);
console.log(err);
}
return wallet;
};
const validateMnemonic = (
mnemonic,
wordlist = BCH.Mnemonic.wordLists().english,
) => {
let mnemonicTestOutput;
try {
mnemonicTestOutput = BCH.Mnemonic.validate(mnemonic, wordlist);
if (mnemonicTestOutput === 'Valid mnemonic') {
return true;
} else {
return false;
}
} catch (err) {
console.log(err);
return false;
}
};
const handleUpdateWallet = async setWallet => {
const wallet = await getWallet();
setWallet(wallet);
};
// Parse for incoming BCH transactions
// Only notify if websocket is not connected
if (
(ws === null || ws.readyState !== 1) &&
previousBalances &&
balances &&
'totalBalance' in previousBalances &&
'totalBalance' in balances &&
new BigNumber(balances.totalBalance)
.minus(previousBalances.totalBalance)
.gt(0)
) {
notification.success({
message: 'Transaction received',
description: (
You received{' '}
{Number(
balances.totalBalance - previousBalances.totalBalance,
).toFixed(8)}{' '}
BCH!
),
duration: 3,
});
}
// Parse for incoming SLP transactions
if (
tokens &&
tokens[0] &&
tokens[0].balance &&
previousTokens &&
previousTokens[0] &&
previousTokens[0].balance
) {
// If tokens length is greater than previousTokens length, a new token has been received
// Note, a user could receive a new token, AND more of existing tokens in between app updates
// In this case, the app will only notify about the new token
// TODO better handling for all possible cases to cover this
// TODO handle with websockets for better response time, less complicated calc
if (tokens.length > previousTokens.length) {
// Find the new token
const tokenIds = tokens.map(({ tokenId }) => tokenId);
const previousTokenIds = previousTokens.map(
({ tokenId }) => tokenId,
);
//console.log(`tokenIds`, tokenIds);
//console.log(`previousTokenIds`, previousTokenIds);
// An array with the new token Id
const newTokenIdArr = tokenIds.filter(
tokenId => !previousTokenIds.includes(tokenId),
);
// It's possible that 2 new tokens were received
// To do, handle this case
const newTokenId = newTokenIdArr[0];
//console.log(newTokenId);
// How much of this tokenId did you get?
// would be at
// Find where the newTokenId is
const receivedTokenObjectIndex = tokens.findIndex(
x => x.tokenId === newTokenId,
);
//console.log(`receivedTokenObjectIndex`, receivedTokenObjectIndex);
// Calculate amount received
//console.log(`receivedTokenObject:`, tokens[receivedTokenObjectIndex]);
const receivedSlpQty = tokens[
receivedTokenObjectIndex
].balance.toString();
const receivedSlpTicker =
tokens[receivedTokenObjectIndex].info.tokenTicker;
const receivedSlpName =
tokens[receivedTokenObjectIndex].info.tokenName;
//console.log(`receivedSlpQty`, receivedSlpQty);
- // Notification
- notification.success({
- message: `SLP Transaction received: ${receivedSlpTicker}`,
- description: (
-
- You received {receivedSlpQty} {receivedSlpName}
-
- ),
- duration: 5,
- });
+ // Notification if you received SLP
+ if (receivedSlpQty > 0) {
+ notification.success({
+ message: `${currency.tokenTicker} Transaction received: ${receivedSlpTicker}`,
+ description: (
+
+ You received {receivedSlpQty} {receivedSlpName}
+
+ ),
+ duration: 5,
+ });
+ }
//
} else {
// If tokens[i].balance > previousTokens[i].balance, a new SLP tx of an existing token has been received
+ // Note that tokens[i].balance is of type BigNumber
for (let i = 0; i < tokens.length; i += 1) {
- if (tokens[i].balance > previousTokens[i].balance) {
+ if (tokens[i].balance.gt(previousTokens[i].balance)) {
// Received this token
// console.log(`previousTokenId`, previousTokens[i].tokenId);
// console.log(`currentTokenId`, tokens[i].tokenId);
+
if (previousTokens[i].tokenId !== tokens[i].tokenId) {
console.log(
`TokenIds do not match, breaking from SLP notifications`,
);
// Then don't send the notification
// Also don't 'continue' ; this means you have sent a token, just stop iterating through
break;
}
- const receivedSlpDecimals = tokens[i].info.decimals;
- const receivedSlpQty = (
- tokens[i].balance - previousTokens[i].balance
- ).toFixed(receivedSlpDecimals);
+ const receivedSlpQty = tokens[i].balance.minus(
+ previousTokens[i].balance,
+ );
+
const receivedSlpTicker = tokens[i].info.tokenTicker;
const receivedSlpName = tokens[i].info.tokenName;
notification.success({
message: `SLP Transaction received: ${receivedSlpTicker}`,
description: (
- You received {receivedSlpQty} {receivedSlpName}
+ You received {receivedSlpQty.toString()}{' '}
+ {receivedSlpName}
),
duration: 5,
});
}
}
}
}
// Update price every 1 min
useAsyncTimeout(async () => {
fetchBchPrice();
}, 60000);
// Update wallet every 10s
useAsyncTimeout(async () => {
const wallet = await getWallet();
update({
wallet,
setWalletState,
}).finally(() => {
setLoading(false);
});
}, 10000);
const initializeWebsocket = (cashAddress, slpAddress) => {
// console.log(`initializeWebsocket(${cashAddress}, ${slpAddress})`);
// This function parses 3 cases
// 1: edge case, websocket is in state but not properly connected
// > Remove it from state and forget about it, fall back to normal notifications
// 2: edge-ish case, websocket is in state and connected but user has changed wallet
// > Unsubscribe from old addresses and subscribe to new ones
// 3: most common: app is opening, creating websocket with existing addresses
// If the websocket is already in state but is not properly connected
if (ws !== null && ws.readyState !== 1) {
// Forget about it and use conventional notifications
// Close
ws.close();
// Remove from state
setWs(null);
}
// If the websocket is in state and connected
else if (ws !== null) {
// console.log(`Websocket already in state`);
// console.log(`ws,`, ws);
// instead of initializing websocket, unsubscribe from old addresses and subscribe to new ones
const previousWsCashAddress = previousWallet.Path145.legacyAddress;
const previousWsSlpAddress = previousWallet.Path245.legacyAddress;
try {
// Unsubscribe from previous addresses
ws.send(
JSON.stringify({
op: 'addr_unsub',
addr: previousWsCashAddress,
}),
);
console.log(
`Unsubscribed from BCH address at ${previousWsCashAddress}`,
);
ws.send(
JSON.stringify({
op: 'addr_unsub',
addr: previousWsSlpAddress,
}),
);
console.log(
`Unsubscribed from SLP address at ${previousWsSlpAddress}`,
);
// Subscribe to new addresses
ws.send(
JSON.stringify({
op: 'addr_sub',
addr: cashAddress,
}),
);
console.log(`Subscribed to BCH address at ${cashAddress}`);
// Subscribe to SLP address
ws.send(
JSON.stringify({
op: 'addr_sub',
addr: slpAddress,
}),
);
console.log(`Subscribed to SLP address at ${slpAddress}`);
// Reset onmessage; it was previously set with the old addresses
// Note this code is exactly identical to lines 431-490
// TODO put in function
ws.onmessage = e => {
// TODO handle case where receive multiple messages on one incoming transaction
//console.log(`ws msg received`);
const incomingTx = JSON.parse(e.data);
console.log(incomingTx);
let bchSatsReceived = 0;
// First, check the inputs
// If cashAddress or slpAddress are in the inputs, then this is a sent tx and should be ignored for notifications
if (
incomingTx &&
incomingTx.x &&
incomingTx.x.inputs &&
incomingTx.x.out
) {
const inputs = incomingTx.x.inputs;
// Iterate over inputs and see if this transaction was sent by the active wallet
for (let i = 0; i < inputs.length; i += 1) {
if (
inputs[i].prev_out.addr === cashAddress ||
inputs[i].prev_out.addr === slpAddress
) {
// console.log(`Found a sending tx, not notifying`);
// This is a sent transaction and should be ignored by notification handlers
return;
}
}
// Iterate over outputs to determine receiving address
const outputs = incomingTx.x.out;
for (let i = 0; i < outputs.length; i += 1) {
if (outputs[i].addr === cashAddress) {
// console.log(`BCH transaction received`);
bchSatsReceived += outputs[i].value;
// handle
}
if (outputs[i].addr === slpAddress) {
console.log(`SLP transaction received`);
//handle
// you would want to get the slp info using this endpoint:
// https://rest.kingbch.com/v3/slp/txDetails/cb39dd04e07e172a37addfcb1d6e167dc52c01867ba21c9bf8b5acf4dd969a3f
// But it does not work for unconfirmed txs
// Hold off on slp tx notifications for now
}
}
}
// parse for receiving address
// if received at cashAddress, parse for BCH amount, notify BCH received
// if received at slpAddress, parse for token, notify SLP received
// if those checks fail, could be from a 'sent' tx, ignore
// Note, when you send an SLP tx, you get SLP change to SLP address and BCH change to BCH address
// Also note, when you send an SLP tx, you often have inputs from both BCH and SLP addresses
// This causes a sent SLP tx to register 4 times from the websocket
// Best way to ignore this is to ignore any incoming utx.x with BCH or SLP address in the inputs
// Notification for received BCH
if (bchSatsReceived > 0) {
notification.success({
message: 'Transaction received',
description: (
You received {bchSatsReceived / 1e8} BCH!
),
duration: 3,
});
}
};
} catch (err) {
console.log(
`Error attempting to configure websocket for new wallet`,
);
console.log(err);
console.log(`Closing connection`);
ws.close();
setWs(null);
}
} else {
// If there is no websocket, create one, subscribe to addresses, and add notifications for incoming BCH transactions
let newWs = new WebSocket('wss://ws.blockchain.info/bch/inv');
newWs.onopen = () => {
console.log(`Connected to bchWs`);
// Subscribe to BCH address
newWs.send(
JSON.stringify({
op: 'addr_sub',
addr: cashAddress,
}),
);
console.log(`Subscribed to BCH address at ${cashAddress}`);
// Subscribe to SLP address
newWs.send(
JSON.stringify({
op: 'addr_sub',
addr: slpAddress,
}),
);
console.log(`Subscribed to SLP address at ${slpAddress}`);
};
newWs.onerror = e => {
// close and set to null
console.log(`Error in websocket connection for ${newWs}`);
console.log(e);
setWs(null);
};
newWs.onclose = () => {
console.log(`Websocket connection closed`);
// Unsubscribe on close to prevent double subscribing
//{"op":"addr_unsub", "addr":"$bitcoin_address"}
newWs.send(
JSON.stringify({
op: 'addr_unsub',
addr: cashAddress,
}),
);
console.log(`Unsubscribed from BCH address at ${cashAddress}`);
newWs.send(
JSON.stringify({
op: 'addr_sub',
addr: slpAddress,
}),
);
console.log(`Unsubscribed from SLP address at ${slpAddress}`);
};
newWs.onmessage = e => {
// TODO handle case where receive multiple messages on one incoming transaction
//console.log(`ws msg received`);
const incomingTx = JSON.parse(e.data);
console.log(incomingTx);
let bchSatsReceived = 0;
// First, check the inputs
// If cashAddress or slpAddress are in the inputs, then this is a sent tx and should be ignored for notifications
if (
incomingTx &&
incomingTx.x &&
incomingTx.x.inputs &&
incomingTx.x.out
) {
const inputs = incomingTx.x.inputs;
// Iterate over inputs and see if this transaction was sent by the active wallet
for (let i = 0; i < inputs.length; i += 1) {
if (
inputs[i].prev_out.addr === cashAddress ||
inputs[i].prev_out.addr === slpAddress
) {
// console.log(`Found a sending tx, not notifying`);
// This is a sent transaction and should be ignored by notification handlers
return;
}
}
// Iterate over outputs to determine receiving address
const outputs = incomingTx.x.out;
for (let i = 0; i < outputs.length; i += 1) {
if (outputs[i].addr === cashAddress) {
// console.log(`BCH transaction received`);
bchSatsReceived += outputs[i].value;
// handle
}
if (outputs[i].addr === slpAddress) {
console.log(`SLP transaction received`);
//handle
// you would want to get the slp info using this endpoint:
// https://rest.kingbch.com/v3/slp/txDetails/cb39dd04e07e172a37addfcb1d6e167dc52c01867ba21c9bf8b5acf4dd969a3f
// But it does not work for unconfirmed txs
// Hold off on slp tx notifications for now
}
}
}
// parse for receiving address
// if received at cashAddress, parse for BCH amount, notify BCH received
// if received at slpAddress, parse for token, notify SLP received
// if those checks fail, could be from a 'sent' tx, ignore
// Note, when you send an SLP tx, you get SLP change to SLP address and BCH change to BCH address
// Also note, when you send an SLP tx, you often have inputs from both BCH and SLP addresses
// This causes a sent SLP tx to register 4 times from the websocket
// Best way to ignore this is to ignore any incoming utx.x with BCH or SLP address in the inputs
// Notification for received BCH
if (bchSatsReceived > 0) {
notification.success({
message: 'Transaction received',
description: (
You received {bchSatsReceived / 1e8} BCH!
),
duration: 3,
});
}
};
setWs(newWs);
}
};
const fetchBchPrice = async () => {
// Split this variable out in case coingecko changes
const cryptoId = currency.coingeckoId;
// Keep currency as a variable as eventually it will be a user setting
const fiatCode = 'usd';
// Keep this in the code, because different URLs will have different outputs require different parsing
const priceApiUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${cryptoId}&vs_currencies=${fiatCode}&include_last_updated_at=true`;
let bchPrice;
let bchPriceJson;
try {
bchPrice = await fetch(priceApiUrl);
//console.log(`bchPrice`, bchPrice);
} catch (err) {
console.log(`Error fetching BCH Price`);
console.log(err);
}
try {
bchPriceJson = await bchPrice.json();
//console.log(`bchPriceJson`, bchPriceJson);
const bchPriceInFiat = bchPriceJson[cryptoId][fiatCode];
//console.log(`bchPriceInFiat`, bchPriceInFiat);
setFiatPrice(bchPriceInFiat);
} catch (err) {
console.log(`Error parsing price API response to JSON`);
console.log(err);
}
};
useEffect(() => {
handleUpdateWallet(setWallet);
fetchBchPrice();
}, []);
useEffect(() => {
if (
wallet &&
wallet.Path145 &&
wallet.Path145.cashAddress &&
wallet.Path245 &&
wallet.Path245.cashAddress
) {
if (currency.useBlockchainWs) {
initializeWebsocket(
wallet.Path145.legacyAddress,
wallet.Path245.legacyAddress,
);
}
}
}, [wallet]);
return {
BCH,
wallet,
fiatPrice,
slpBalancesAndUtxos,
balances,
tokens,
txHistory,
loading,
apiError,
getWallet,
validateMnemonic,
getWalletDetails,
getSavedWallets,
update: async () =>
update({
wallet: await getWallet(),
setLoading,
setWalletState,
}),
createWallet: async importMnemonic => {
setLoading(true);
const newWallet = await createWallet(importMnemonic);
setWallet(newWallet);
update({
wallet: newWallet,
setWalletState,
}).finally(() => setLoading(false));
},
activateWallet: async walletToActivate => {
setLoading(true);
const newWallet = await activateWallet(walletToActivate);
setWallet(newWallet);
update({
wallet: newWallet,
setWalletState,
}).finally(() => setLoading(false));
},
addNewSavedWallet,
renameWallet,
deleteWallet,
};
};
export default useWallet;