Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/hooks/useBCH.js
import BigNumber from 'bignumber.js'; | import BigNumber from 'bignumber.js'; | ||||
import { | import { | ||||
currency, | currency, | ||||
isCashtabOutput, | isCashtabOutput, | ||||
isEtokenOutput, | isEtokenOutput, | ||||
extractCashtabMessage, | extractCashtabMessage, | ||||
extractExternalMessage, | extractExternalMessage, | ||||
isMessageEncrypted, | |||||
extractEncryptedMessage, | |||||
} from '@components/Common/Ticker'; | } from '@components/Common/Ticker'; | ||||
import { isValidTokenStats } from '@utils/validation'; | import { isValidTokenStats } from '@utils/validation'; | ||||
import SlpWallet from 'minimal-slp-wallet'; | import SlpWallet from 'minimal-slp-wallet'; | ||||
import { | import { | ||||
toSmallestDenomination, | toSmallestDenomination, | ||||
fromSmallestDenomination, | fromSmallestDenomination, | ||||
batchArray, | batchArray, | ||||
flattenBatchedHydratedUtxos, | flattenBatchedHydratedUtxos, | ||||
isValidStoredWallet, | isValidStoredWallet, | ||||
checkNullUtxosForTokenStatus, | checkNullUtxosForTokenStatus, | ||||
confirmNonEtokenUtxos, | confirmNonEtokenUtxos, | ||||
} from '@utils/cashMethods'; | } from '@utils/cashMethods'; | ||||
import cashaddr from 'ecashaddrjs'; | import cashaddr from 'ecashaddrjs'; | ||||
const eccrypto = require('eccrypto-js'); | |||||
export default function useBCH() { | export default function useBCH() { | ||||
const SEND_BCH_ERRORS = { | const SEND_BCH_ERRORS = { | ||||
INSUFFICIENT_FUNDS: 0, | INSUFFICIENT_FUNDS: 0, | ||||
NETWORK_ERROR: 1, | NETWORK_ERROR: 1, | ||||
INSUFFICIENT_PRIORITY: 66, // ~insufficient fee | INSUFFICIENT_PRIORITY: 66, // ~insufficient fee | ||||
DOUBLE_SPENDING: 18, | DOUBLE_SPENDING: 18, | ||||
MAX_UNCONFIRMED_TXS: 64, | MAX_UNCONFIRMED_TXS: 64, | ||||
▲ Show 20 Lines • Show All 44 Lines • ▼ Show 20 Lines | ) => { | ||||
// Sort with most recent transaction at index 0 | // Sort with most recent transaction at index 0 | ||||
flatTxHistory.sort((a, b) => b.height - a.height); | flatTxHistory.sort((a, b) => b.height - a.height); | ||||
// Only return 10 | // Only return 10 | ||||
return flatTxHistory.splice(0, txCount); | return flatTxHistory.splice(0, txCount); | ||||
}; | }; | ||||
const parseTxData = txData => { | const parseTxData = async (txData, wallet) => { | ||||
/* | /* | ||||
Desired output | Desired output | ||||
[ | [ | ||||
{ | { | ||||
txid: '', | txid: '', | ||||
type: send, receive | type: send, receive | ||||
receivingAddress: '', | receivingAddress: '', | ||||
quantity: amount bcha | quantity: amount bcha | ||||
token: true/false | token: true/false | ||||
tokenInfo: { | tokenInfo: { | ||||
tokenId: | tokenId: | ||||
tokenQty: | tokenQty: | ||||
txType: mint, send, other | txType: mint, send, other | ||||
} | } | ||||
opReturnMessage: 'message extracted from asm' or '' | opReturnMessage: 'message extracted from asm' or '' | ||||
} | } | ||||
] | ] | ||||
*/ | */ | ||||
const parsedTxHistory = []; | const parsedTxHistory = []; | ||||
for (let i = 0; i < txData.length; i += 1) { | for (let i = 0; i < txData.length; i += 1) { | ||||
const tx = txData[i]; | const tx = txData[i]; | ||||
const parsedTx = {}; | const parsedTx = {}; | ||||
// Move over info that does not need to be calculated | // Move over info that does not need to be calculated | ||||
parsedTx.txid = tx.txid; | parsedTx.txid = tx.txid; | ||||
Show All 10 Lines | const parseTxData = async (txData, wallet) => { | ||||
} | } | ||||
parsedTx.confirmations = tx.confirmations; | parsedTx.confirmations = tx.confirmations; | ||||
parsedTx.blocktime = tx.blocktime; | parsedTx.blocktime = tx.blocktime; | ||||
let amountSent = 0; | let amountSent = 0; | ||||
let amountReceived = 0; | let amountReceived = 0; | ||||
let opReturnMessage = ''; | let opReturnMessage = ''; | ||||
let isCashtabMessage = false; | let isCashtabMessage = false; | ||||
let isEncryptedMessage = false; | |||||
// Assume an incoming transaction | // Assume an incoming transaction | ||||
let outgoingTx = false; | let outgoingTx = false; | ||||
let tokenTx = false; | let tokenTx = false; | ||||
let substring = ''; | let substring = ''; | ||||
// get the address of the sender for this tx and encode into eCash address | // get the address of the sender for this tx and encode into eCash address | ||||
let senderBchAddress = tx.vin[0].address; | let senderBchAddress = tx.vin[0].address; | ||||
const { prefix, type, hash } = cashaddr.decode(senderBchAddress); | const { prefix, type, hash } = cashaddr.decode(senderBchAddress); | ||||
Show All 12 Lines | const parseTxData = async (txData, wallet) => { | ||||
for (let j = 0; j < tx.vout.length; j += 1) { | for (let j = 0; j < tx.vout.length; j += 1) { | ||||
const thisOutput = tx.vout[j]; | const thisOutput = tx.vout[j]; | ||||
// If there is no addresses object in the output, it's either an OP_RETURN msg or token tx | // If there is no addresses object in the output, it's either an OP_RETURN msg or token tx | ||||
if ( | if ( | ||||
!Object.keys(thisOutput.scriptPubKey).includes('addresses') | !Object.keys(thisOutput.scriptPubKey).includes('addresses') | ||||
) { | ) { | ||||
let hex = thisOutput.scriptPubKey.hex; | let hex = thisOutput.scriptPubKey.hex; | ||||
console.log('Full hex is: ' + hex); | |||||
if (isEtokenOutput(hex)) { | if (isEtokenOutput(hex)) { | ||||
// this is an eToken transaction | // this is an eToken transaction | ||||
tokenTx = true; | tokenTx = true; | ||||
} else if (isCashtabOutput(hex)) { | } else if (isCashtabOutput(hex)) { | ||||
// this is a cashtab.com generated message | // this is a cashtab.com generated message | ||||
try { | console.log('Cashtab message found'); | ||||
substring = extractCashtabMessage(hex); | substring = extractCashtabMessage(hex); | ||||
opReturnMessage = Buffer.from(substring, 'hex'); | |||||
isCashtabMessage = true; | if (isMessageEncrypted(substring)) { | ||||
// this is an encrypted Cashtab message | |||||
console.log('encrypted message found'); | |||||
substring = extractEncryptedMessage(substring); | |||||
console.log( | |||||
'extracted encryption string is: ' + substring, | |||||
); | |||||
let fundingWif; | |||||
if ( | |||||
wallet && | |||||
wallet.state && | |||||
wallet.state.slpBalancesAndUtxos | |||||
) { | |||||
fundingWif = | |||||
wallet.state.slpBalancesAndUtxos | |||||
.nonSlpUtxos[0].wif; | |||||
} else { | |||||
break; | |||||
} | |||||
console.log('WIF for decryption: ' + fundingWif); | |||||
// Convert the hex encoded message to a buffer | |||||
const msgBuf = Buffer.from(substring, 'hex'); | |||||
let structData; | |||||
let decryptedMessage; | |||||
try { | |||||
// Convert the bufer into a structured object. | |||||
structData = convertToEncryptStruct(msgBuf); | |||||
decryptedMessage = await eccrypto.decrypt( | |||||
fundingWif, | |||||
structData, | |||||
); | |||||
console.log( | |||||
'decryption successful, message: ' + | |||||
decryptedMessage, | |||||
); | |||||
} catch (err) { | } catch (err) { | ||||
// soft error if an unexpected or invalid cashtab hex is encountered | console.log('parseTx decryption error: ' + err); | ||||
opReturnMessage = ''; | decryptedMessage = 'Encrypted Message'; | ||||
} | |||||
console.log( | console.log( | ||||
'useBCH.parsedTxHistory() error: invalid cashtab msg hex: ' + | 'parseTx.Decrypted message:' + decryptedMessage, | ||||
substring, | |||||
); | ); | ||||
opReturnMessage = decryptedMessage; | |||||
isEncryptedMessage = true; | |||||
} else { | |||||
// this is an un-encrypted Cashtab message | |||||
opReturnMessage = substring; | |||||
} | } | ||||
isCashtabMessage = true; | |||||
} else { | } else { | ||||
// this is an externally generated message | // this is an externally generated message | ||||
console.log('Un-encrypted message found'); | |||||
try { | try { | ||||
substring = extractExternalMessage(hex); | substring = extractExternalMessage(hex); | ||||
opReturnMessage = Buffer.from(substring, 'hex'); | opReturnMessage = Buffer.from(substring, 'hex'); | ||||
} catch (err) { | } catch (err) { | ||||
// soft error if an unexpected or invalid cashtab hex is encountered | // soft error if an unexpected or invalid cashtab hex is encountered | ||||
opReturnMessage = ''; | opReturnMessage = ''; | ||||
console.log( | console.log( | ||||
'useBCH.parsedTxHistory() error: invalid external msg hex: ' + | 'useBCH.parsedTxHistory() error: invalid external msg hex: ' + | ||||
Show All 22 Lines | const parseTxData = async (txData, wallet) => { | ||||
parsedTx.amountSent = amountSent; | parsedTx.amountSent = amountSent; | ||||
parsedTx.amountReceived = amountReceived; | parsedTx.amountReceived = amountReceived; | ||||
parsedTx.tokenTx = tokenTx; | parsedTx.tokenTx = tokenTx; | ||||
parsedTx.outgoingTx = outgoingTx; | parsedTx.outgoingTx = outgoingTx; | ||||
parsedTx.replyAddress = senderAddress; | parsedTx.replyAddress = senderAddress; | ||||
parsedTx.destinationAddress = destinationAddress; | parsedTx.destinationAddress = destinationAddress; | ||||
parsedTx.opReturnMessage = opReturnMessage; | parsedTx.opReturnMessage = opReturnMessage; | ||||
parsedTx.isCashtabMessage = isCashtabMessage; | parsedTx.isCashtabMessage = isCashtabMessage; | ||||
parsedTx.isEncryptedMessage = isEncryptedMessage; | |||||
parsedTxHistory.push(parsedTx); | parsedTxHistory.push(parsedTx); | ||||
} | } | ||||
return parsedTxHistory; | return parsedTxHistory; | ||||
}; | }; | ||||
// Converts a serialized buffer containing encrypted data into an object | |||||
// that can interpreted by the eccryptoJS library. | |||||
const convertToEncryptStruct = encbuf => { | |||||
try { | |||||
let offset = 0; | |||||
const tagLength = 32; | |||||
let pub; | |||||
switch (encbuf[0]) { | |||||
case 4: | |||||
pub = encbuf.slice(0, 65); | |||||
break; | |||||
case 3: | |||||
case 2: | |||||
pub = encbuf.slice(0, 33); | |||||
break; | |||||
default: | |||||
throw new Error(`Invalid type: ${encbuf[0]}`); | |||||
} | |||||
offset += pub.length; | |||||
const c = encbuf.slice(offset, encbuf.length - tagLength); | |||||
const ivbuf = c.slice(0, 128 / 8); | |||||
const ctbuf = c.slice(128 / 8); | |||||
const d = encbuf.slice(encbuf.length - tagLength, encbuf.length); | |||||
return { | |||||
iv: ivbuf, | |||||
ephemPublicKey: pub, | |||||
ciphertext: ctbuf, | |||||
mac: d, | |||||
}; | |||||
} catch (err) { | |||||
console.error(`Error in convertToEncryptStruct()`); | |||||
throw err; | |||||
} | |||||
}; | |||||
const getTxHistory = async (BCH, addresses) => { | const getTxHistory = async (BCH, addresses) => { | ||||
let txHistoryResponse; | let txHistoryResponse; | ||||
try { | try { | ||||
//console.log(`API Call: BCH.Electrumx.utxo(addresses)`); | //console.log(`API Call: BCH.Electrumx.utxo(addresses)`); | ||||
//console.log(addresses); | //console.log(addresses); | ||||
txHistoryResponse = await BCH.Electrumx.transactions(addresses); | txHistoryResponse = await BCH.Electrumx.transactions(addresses); | ||||
//console.log(`BCH.Electrumx.transactions(addresses) succeeded`); | //console.log(`BCH.Electrumx.transactions(addresses) succeeded`); | ||||
//console.log(`txHistoryResponse`, txHistoryResponse); | //console.log(`txHistoryResponse`, txHistoryResponse); | ||||
Show All 26 Lines | const getTxDataWithPassThrough = async (BCH, flatTx) => { | ||||
// Include txid if you don't get it from the attempted response | // Include txid if you don't get it from the attempted response | ||||
txDataWithPassThrough.txid = flatTx.txid; | txDataWithPassThrough.txid = flatTx.txid; | ||||
} | } | ||||
txDataWithPassThrough.height = flatTx.height; | txDataWithPassThrough.height = flatTx.height; | ||||
txDataWithPassThrough.address = flatTx.address; | txDataWithPassThrough.address = flatTx.address; | ||||
return txDataWithPassThrough; | return txDataWithPassThrough; | ||||
}; | }; | ||||
const getTxData = async (BCH, txHistory) => { | const getTxData = async (BCH, txHistory, wallet) => { | ||||
// Flatten tx history | // Flatten tx history | ||||
let flatTxs = flattenTransactions(txHistory); | let flatTxs = flattenTransactions(txHistory); | ||||
// Build array of promises to get tx data for all 10 transactions | // Build array of promises to get tx data for all 10 transactions | ||||
let txDataPromises = []; | let txDataPromises = []; | ||||
for (let i = 0; i < flatTxs.length; i += 1) { | for (let i = 0; i < flatTxs.length; i += 1) { | ||||
const txDataPromise = await getTxDataWithPassThrough( | const txDataPromise = await getTxDataWithPassThrough( | ||||
BCH, | BCH, | ||||
flatTxs[i], | flatTxs[i], | ||||
); | ); | ||||
txDataPromises.push(txDataPromise); | txDataPromises.push(txDataPromise); | ||||
} | } | ||||
// Get txData for the 10 most recent transactions | // Get txData for the 10 most recent transactions | ||||
let txDataPromiseResponse; | let txDataPromiseResponse; | ||||
try { | try { | ||||
txDataPromiseResponse = await Promise.all(txDataPromises); | txDataPromiseResponse = await Promise.all(txDataPromises); | ||||
const parsed = parseTxData(txDataPromiseResponse); | const parsed = parseTxData(txDataPromiseResponse, wallet); | ||||
return parsed; | return parsed; | ||||
} catch (err) { | } catch (err) { | ||||
console.log(`Error in Promise.all(txDataPromises):`); | console.log(`Error in Promise.all(txDataPromises):`); | ||||
console.log(err); | console.log(err); | ||||
return err; | return err; | ||||
} | } | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 674 Lines • ▼ Show 20 Lines | |||||
const sendBch = async ( | const sendBch = async ( | ||||
BCH, | BCH, | ||||
wallet, | wallet, | ||||
utxos, | utxos, | ||||
destinationAddress, | destinationAddress, | ||||
sendAmount, | sendAmount, | ||||
feeInSatsPerByte, | feeInSatsPerByte, | ||||
optionalOpReturnMsg, | optionalOpReturnMsg, | ||||
encryptionFlag, | |||||
) => { | ) => { | ||||
try { | try { | ||||
if (!sendAmount) { | if (!sendAmount) { | ||||
return null; | return null; | ||||
} | } | ||||
const value = new BigNumber(sendAmount); | const value = new BigNumber(sendAmount); | ||||
Show All 22 Lines | ) => { | ||||
// Throw validation error if toSmallestDenomination returns false | // Throw validation error if toSmallestDenomination returns false | ||||
if (!satoshisToSend) { | if (!satoshisToSend) { | ||||
const error = new Error( | const error = new Error( | ||||
`Invalid decimal places for send amount`, | `Invalid decimal places for send amount`, | ||||
); | ); | ||||
throw error; | throw error; | ||||
} | } | ||||
let script; | |||||
// Start of building the OP_RETURN output. | // Start of building the OP_RETURN output. | ||||
// only build the OP_RETURN output if the user supplied it | // only build the OP_RETURN output if the user supplied it | ||||
if ( | if ( | ||||
typeof optionalOpReturnMsg !== 'undefined' && | typeof optionalOpReturnMsg !== 'undefined' && | ||||
optionalOpReturnMsg.trim() !== '' | optionalOpReturnMsg.trim() !== '' | ||||
) { | ) { | ||||
const script = [ | if (encryptionFlag) { | ||||
//if the user has opted to encrypt this message | |||||
let recipientPubKey = await getPublicKey( | |||||
BCH, | |||||
destinationAddress, | |||||
); | |||||
console.log('PUBLIC KEY IS: ' + recipientPubKey); | |||||
const pubKeyBuf = Buffer.from(recipientPubKey, 'hex'); | |||||
const bufferedFile = Buffer.from(optionalOpReturnMsg); | |||||
const structuredEj = await eccrypto.encrypt( | |||||
pubKeyBuf, | |||||
bufferedFile, | |||||
); | |||||
// Serialize the encrypted data object | |||||
const encryptedEj = Buffer.concat([ | |||||
structuredEj.ephemPublicKey, | |||||
structuredEj.iv, | |||||
structuredEj.ciphertext, | |||||
structuredEj.mac, | |||||
]); | |||||
const encryptedMessage = encryptedEj.toString('hex'); | |||||
console.log('Original message: ' + optionalOpReturnMsg); | |||||
console.log('Encrypted message: ' + encryptedMessage); | |||||
// build the OP_RETURN script with the encryption prefix | |||||
script = [ | |||||
BCH.Script.opcodes.OP_RETURN, // 6a | |||||
Buffer.from( | |||||
currency.opReturn.appPrefixesHex.cashtab, | |||||
'hex', | |||||
), // 00746162 | |||||
Buffer.from(currency.opReturn.encryptionPrefix, 'hex'), // 656e6372797074 a.k.a 'encrypt' | |||||
Buffer.from(encryptedEj), | |||||
]; | |||||
} else { | |||||
// this is an un-encrypted message | |||||
script = [ | |||||
BCH.Script.opcodes.OP_RETURN, // 6a | BCH.Script.opcodes.OP_RETURN, // 6a | ||||
Buffer.from( | Buffer.from( | ||||
currency.opReturn.appPrefixesHex.cashtab, | currency.opReturn.appPrefixesHex.cashtab, | ||||
'hex', | 'hex', | ||||
), // 00746162 | ), // 00746162 | ||||
Buffer.from(optionalOpReturnMsg), | Buffer.from(optionalOpReturnMsg), | ||||
]; | ]; | ||||
} | |||||
const data = BCH.Script.encode(script); | const data = BCH.Script.encode(script); | ||||
transactionBuilder.addOutput(data, 0); | transactionBuilder.addOutput(data, 0); | ||||
} | } | ||||
// End of building the OP_RETURN output. | // End of building the OP_RETURN output. | ||||
let originalAmount = new BigNumber(0); | let originalAmount = new BigNumber(0); | ||||
let txFee = 0; | let txFee = 0; | ||||
for (let i = 0; i < utxos.length; i++) { | for (let i = 0; i < utxos.length; i++) { | ||||
▲ Show 20 Lines • Show All 96 Lines • ▼ Show 20 Lines | ) => { | ||||
) { | ) { | ||||
err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; | err.code = SEND_BCH_ERRORS.MAX_UNCONFIRMED_TXS; | ||||
} | } | ||||
console.log(`error: `, err); | console.log(`error: `, err); | ||||
throw err; | throw err; | ||||
} | } | ||||
}; | }; | ||||
const getPublicKey = async (BCH, address) => { | |||||
try { | |||||
const publicKey = await BCH.encryption.getPubKey(address); | |||||
return publicKey.publicKey; | |||||
} catch (err) { | |||||
console.log(`useBCH.getPublicKey() error: `, err); | |||||
throw err; | |||||
} | |||||
}; | |||||
const getBCH = (apiIndex = 0) => { | const getBCH = (apiIndex = 0) => { | ||||
let ConstructedSlpWallet; | let ConstructedSlpWallet; | ||||
ConstructedSlpWallet = new SlpWallet('', { | ConstructedSlpWallet = new SlpWallet('', { | ||||
restURL: getRestUrl(apiIndex), | restURL: getRestUrl(apiIndex), | ||||
}); | }); | ||||
return ConstructedSlpWallet.bchjs; | return ConstructedSlpWallet.bchjs; | ||||
}; | }; | ||||
Show All 21 Lines |