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 Long from 'long'; | |||||
import { ChronikClient } from 'chronik-client'; | |||||
import { currency } from 'components/Common/Ticker'; | import { currency } 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, | ||||
convertToEncryptStruct, | convertToEncryptStruct, | ||||
getPublicKey, | getPublicKey, | ||||
parseOpReturn, | parseOpReturn, | ||||
} from 'utils/cashMethods'; | } from 'utils/cashMethods'; | ||||
import cashaddr from 'ecashaddrjs'; | import cashaddr from 'ecashaddrjs'; | ||||
import ecies from 'ecies-lite'; | import ecies from 'ecies-lite'; | ||||
import wif from 'wif'; | import wif from 'wif'; | ||||
const chronik = new ChronikClient('https://chronik.be.cash/xec'); | |||||
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 627 Lines • ▼ Show 20 Lines | const returnHydrateUtxosPromise = (BCH, utxoSetForThisPromise) => { | ||||
}, | }, | ||||
err => { | err => { | ||||
reject(err); | reject(err); | ||||
}, | }, | ||||
); | ); | ||||
}); | }); | ||||
}; | }; | ||||
const getUtxosSingleHashChronik = async hash160 => { | |||||
let utxos; | |||||
try { | |||||
utxos = await chronik.script('p2pkh', hash160).utxos(); | |||||
if (utxos.length === 0) { | |||||
// This seems to be what chronik returns if no utxos at address | |||||
return []; | |||||
} | |||||
return utxos[0].utxos; | |||||
} catch (err) { | |||||
console.log(`Error in chronik.utxos(${hash160})`); | |||||
console.log(err); | |||||
} | |||||
}; | |||||
const getTxDetailsChronik = async txid => { | |||||
let txDetails; | |||||
try { | |||||
txDetails = await chronik.tx(txid); | |||||
return txDetails; | |||||
} catch (err) { | |||||
console.log(`Error in chronik.tx(${txid})`); | |||||
console.log(err); | |||||
} | |||||
}; | |||||
const returnGetUtxosChronikPromise = hashObj => { | |||||
return new Promise((resolve, reject) => { | |||||
getUtxosSingleHashChronik(hashObj.hash160).then( | |||||
result => { | |||||
// Add the address to each utxo | |||||
for (let i = 0; i < result.length; i += 1) { | |||||
const thisUtxo = result[i]; | |||||
thisUtxo.address = hashObj.address; | |||||
} | |||||
resolve(result); | |||||
}, | |||||
err => { | |||||
reject(err); | |||||
}, | |||||
); | |||||
}); | |||||
}; | |||||
const returnGetTokenInfoChronikPromise = tokenId => { | |||||
return new Promise((resolve, reject) => { | |||||
getTxDetailsChronik(tokenId).then( | |||||
result => { | |||||
const thisTokenInfo = result.slpTxData.genesisInfo; | |||||
thisTokenInfo.tokenId = tokenId; | |||||
// You only want the genesis info for tokenId | |||||
resolve(thisTokenInfo); | |||||
}, | |||||
err => { | |||||
reject(err); | |||||
}, | |||||
); | |||||
}); | |||||
}; | |||||
const fetchTxDataForNullUtxos = async (BCH, nullUtxos) => { | const fetchTxDataForNullUtxos = async (BCH, nullUtxos) => { | ||||
// Check nullUtxos. If they aren't eToken txs, count them | // Check nullUtxos. If they aren't eToken txs, count them | ||||
console.log( | console.log( | ||||
`Null utxos found, checking OP_RETURN fields to confirm they are not eToken txs.`, | `Null utxos found, checking OP_RETURN fields to confirm they are not eToken txs.`, | ||||
); | ); | ||||
const txids = []; | const txids = []; | ||||
for (let i = 0; i < nullUtxos.length; i += 1) { | for (let i = 0; i < nullUtxos.length; i += 1) { | ||||
// Batch API call to get their OP_RETURN asm info | // Batch API call to get their OP_RETURN asm info | ||||
▲ Show 20 Lines • Show All 209 Lines • ▼ Show 20 Lines | const createToken = async (BCH, wallet, feeInSatsPerByte, configObj) => { | ||||
transactionBuilder = new BCH.TransactionBuilder(); | transactionBuilder = new BCH.TransactionBuilder(); | ||||
else transactionBuilder = new BCH.TransactionBuilder('testnet'); | else transactionBuilder = new BCH.TransactionBuilder('testnet'); | ||||
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++) { | ||||
const utxo = utxos[i]; | const utxo = utxos[i]; | ||||
originalAmount = originalAmount.plus(new BigNumber(utxo.value)); | |||||
originalAmount = originalAmount.plus( | |||||
new BigNumber(new Long(utxo.value).toString()), | |||||
); | |||||
const vout = utxo.vout; | const vout = utxo.vout; | ||||
const txid = utxo.txid; | const txid = utxo.txid; | ||||
// add input with txid and index of vout | // add input with txid and index of vout | ||||
transactionBuilder.addInput(txid, vout); | transactionBuilder.addInput(txid, vout); | ||||
inputUtxos.push(utxo); | inputUtxos.push(utxo); | ||||
txFee = calcFee(BCH, inputUtxos, 3, feeInSatsPerByte); | txFee = calcFee(BCH, inputUtxos, 3, feeInSatsPerByte); | ||||
▲ Show 20 Lines • Show All 506 Lines • ▼ Show 20 Lines | const sendXec = async ( | ||||
isOneToMany, | isOneToMany, | ||||
destinationAddressAndValueArray, | destinationAddressAndValueArray, | ||||
destinationAddress, | destinationAddress, | ||||
sendAmount, | sendAmount, | ||||
encryptionFlag, | encryptionFlag, | ||||
airdropFlag, | airdropFlag, | ||||
airdropTokenId, | airdropTokenId, | ||||
) => { | ) => { | ||||
console.log(`utxos in sendXEC()`, utxos); | |||||
try { | try { | ||||
let value = new BigNumber(0); | let value = new BigNumber(0); | ||||
if (isOneToMany) { | if (isOneToMany) { | ||||
// this is a one to many XEC transaction | // this is a one to many XEC transaction | ||||
if ( | if ( | ||||
!destinationAddressAndValueArray || | !destinationAddressAndValueArray || | ||||
!destinationAddressAndValueArray.length | !destinationAddressAndValueArray.length | ||||
▲ Show 20 Lines • Show All 147 Lines • ▼ Show 20 Lines | ) => { | ||||
} | } | ||||
} | } | ||||
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 testOriginalAmount = new BigNumber(0); | |||||
let txFee = 0; | let txFee = 0; | ||||
// A normal tx will have 2 outputs, destination and change | // A normal tx will have 2 outputs, destination and change | ||||
// A one to many tx will have n outputs + 1 change output, where n is the number of recipients | // A one to many tx will have n outputs + 1 change output, where n is the number of recipients | ||||
const txOutputs = isOneToMany | const txOutputs = isOneToMany | ||||
? destinationAddressAndValueArray.length + 1 | ? destinationAddressAndValueArray.length + 1 | ||||
: 2; | : 2; | ||||
for (let i = 0; i < utxos.length; i++) { | for (let i = 0; i < utxos.length; i++) { | ||||
const utxo = utxos[i]; | const utxo = utxos[i]; | ||||
originalAmount = originalAmount.plus(utxo.value); | console.log(`utxo in loop`, utxo); | ||||
const vout = utxo.vout; | console.log( | ||||
const txid = utxo.txid; | `new Long(utxo.value).toString(10)`, | ||||
new Long(utxo.value).toString(10), | |||||
); | |||||
console.log(`utxo.value.toString()`, utxo.value.toString()); | |||||
console.log( | |||||
`utxo.value.toString`, | |||||
new Long(utxo.value).toString(10), | |||||
); | |||||
originalAmount = originalAmount.plus( | |||||
new BigNumber(new Long(utxo.value).toString()), | |||||
); | |||||
testOriginalAmount = testOriginalAmount.plus(utxo.value); | |||||
console.log(`originalAmount`, originalAmount.toString()); | |||||
console.log( | |||||
`testOriginalAmount`, | |||||
testOriginalAmount.toString(), | |||||
); | |||||
const vout = utxo.outpoint.outIdx; | |||||
const txid = utxo.outpoint.txid; | |||||
// add input with txid and index of vout | // add input with txid and index of vout | ||||
transactionBuilder.addInput(txid, vout); | transactionBuilder.addInput(txid, vout); | ||||
inputUtxos.push(utxo); | inputUtxos.push(utxo); | ||||
txFee = calcFee(BCH, inputUtxos, txOutputs, feeInSatsPerByte); | txFee = calcFee(BCH, inputUtxos, txOutputs, feeInSatsPerByte); | ||||
if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { | if (originalAmount.minus(satoshisToSend).minus(txFee).gte(0)) { | ||||
break; | break; | ||||
▲ Show 20 Lines • Show All 59 Lines • ▼ Show 20 Lines | ) => { | ||||
// Sign the transactions with the HD node. | // Sign the transactions with the HD node. | ||||
for (let i = 0; i < inputUtxos.length; i++) { | for (let i = 0; i < inputUtxos.length; i++) { | ||||
const utxo = inputUtxos[i]; | const utxo = inputUtxos[i]; | ||||
transactionBuilder.sign( | transactionBuilder.sign( | ||||
i, | i, | ||||
BCH.ECPair.fromWIF(utxo.wif), | BCH.ECPair.fromWIF(utxo.wif), | ||||
undefined, | undefined, | ||||
transactionBuilder.hashTypes.SIGHASH_ALL, | transactionBuilder.hashTypes.SIGHASH_ALL, | ||||
utxo.value, | parseInt(new Long(utxo.value).toString()), | ||||
); | ); | ||||
} | } | ||||
// build tx | // build tx | ||||
const tx = transactionBuilder.build(); | const tx = transactionBuilder.build(); | ||||
// output rawhex | // output rawhex | ||||
const hex = tx.toHex(); | const hex = tx.toHex(); | ||||
Show All 34 Lines | 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; | ||||
}; | }; | ||||
const addressesToHashes = (BCH, addressArr) => { | |||||
// Convert addresses to hash160, the supported input of chronik's utxo method | |||||
console.log(`addressArr`, addressArr); | |||||
const hashObjArray = []; | |||||
for (let i = 0; i < addressArr.length; i += 1) { | |||||
const thisAddress = addressArr[i]; | |||||
const thisHashAddr = BCH.Address.toHash160(thisAddress); | |||||
// You still need to keep track of the address though, as the individual utxos need to have this info | |||||
const thisAddrHashObj = { | |||||
address: thisAddress, | |||||
hash160: thisHashAddr, | |||||
}; | |||||
hashObjArray.push(thisAddrHashObj); | |||||
} | |||||
return hashObjArray; | |||||
}; | |||||
const getUtxosChronik = async (BCH, addresses) => { | |||||
// Convert addresses to hash160 | |||||
// get utxos (promise.all?) | |||||
const hashObjArr = addressesToHashes(BCH, addresses); | |||||
console.log(`hashObjArr`, hashObjArr); | |||||
// get utxos | |||||
const chronikUtxoPromises = []; | |||||
for (let i = 0; i < hashObjArr.length; i += 1) { | |||||
const thisPromise = returnGetUtxosChronikPromise(hashObjArr[i]); | |||||
chronikUtxoPromises.push(thisPromise); | |||||
} | |||||
const allUtxos = await Promise.all(chronikUtxoPromises); | |||||
// Since each individual utxo has address information, no need to keep them in distinct arrays | |||||
// Combine into one array of all utxos | |||||
const flatUtxos = allUtxos.flat(); | |||||
return flatUtxos; | |||||
}; | |||||
const getSlpBalancesAndUtxosFromChronik = chronikUtxos => { | |||||
/* Sample input | |||||
[ | |||||
{ | |||||
"outpoint": { | |||||
"txid": "976753770d4fd3baa0a36e0792ba6b0f906efc771b25690b5300f5437ba0f0db", | |||||
"outIdx": 2 | |||||
}, | |||||
"blockHeight": 660149, | |||||
"isCoinbase": false, | |||||
"value": { | |||||
"low": 546, | |||||
"high": 0, | |||||
"unsigned": false | |||||
}, | |||||
"network": "XEC", | |||||
"address": "bitcoincash:qpv9fx6mjdpgltygudnpw3tvmxdyzx7savhphtzswu" | |||||
}, | |||||
{ | |||||
"outpoint": { | |||||
"txid": "4064e02fe523cb107fecaf3f5abaabb89f7e2bb6662751ba4f86f8d18ebeb1fa", | |||||
"outIdx": 1 | |||||
}, | |||||
"blockHeight": 670076, | |||||
"isCoinbase": false, | |||||
"value": { | |||||
"low": 546, | |||||
"high": 0, | |||||
"unsigned": false | |||||
}, | |||||
"slpMeta": { | |||||
"tokenType": "FUNGIBLE", | |||||
"txType": "SEND", | |||||
"tokenId": "bfddfcfc9fb9a8d61ed74fa94b5e32ccc03305797eea461658303df5805578ef" | |||||
}, | |||||
"slpToken": { | |||||
"amount": { | |||||
"low": 1, | |||||
"high": 0, | |||||
"unsigned": true | |||||
}, | |||||
"isMintBaton": false | |||||
}, | |||||
"network": "XEC", | |||||
"address": "bitcoincash:qpmytrdsakt0axrrlswvaj069nat3p9s7ct4lsf8k9" | |||||
}, | |||||
] | |||||
Desired Output | |||||
{ | |||||
"slpBalancesAndUtxos": { | |||||
"tokens": [ | |||||
{ | |||||
"info": { | |||||
"height": 661700, | |||||
"tx_hash": "854d49d29819cdb5c4d9248146ffc82771cd3a7727f25a22993456f68050503e", | |||||
"tx_pos": 1, | |||||
"value": 546, | |||||
"txid": "854d49d29819cdb5c4d9248146ffc82771cd3a7727f25a22993456f68050503e", | |||||
"vout": 1, | |||||
"utxoType": "token", | |||||
"transactionType": "send", | |||||
"tokenId": "d4ffc597cb08b8c929e464f84069b9009649c7514860f673da48b1b3eba5b56e", | |||||
"tokenTicker": "JoeyTest2", | |||||
"tokenName": "Jt2", | |||||
"tokenDocumentUrl": "thecryptoguy.com", | |||||
"tokenDocumentHash": "", | |||||
"decimals": 0, | |||||
"tokenType": 1, | |||||
"isValid": true, | |||||
"tokenQty": "1", | |||||
"address": "bitcoincash:qpv9fx6mjdpgltygudnpw3tvmxdyzx7savhphtzswu" | |||||
}, | |||||
"tokenId": "d4ffc597cb08b8c929e464f84069b9009649c7514860f673da48b1b3eba5b56e", | |||||
"balance": "1", | |||||
"hasBaton": false | |||||
}, | |||||
] | |||||
}, | |||||
} | |||||
1 - You need to pass the addresses along with the arrays | |||||
2 - Helper function to bump everything into one array and add the address to each utxo | |||||
3 - Construct the `nonSlpUtxos` and `slpUtxos` arrays | |||||
4 - iterate over `slpUtxos` to build the `tokens` array | |||||
*/ | |||||
const nonSlpUtxos = []; | |||||
const slpUtxos = []; | |||||
for (let i = 0; i < chronikUtxos.length; i += 1) { | |||||
// Construct nonSlpUtxos and slpUtxos arrays | |||||
const thisUtxo = chronikUtxos[i]; | |||||
const isEtoken = typeof thisUtxo.slpToken !== 'undefined'; | |||||
if (isEtoken) { | |||||
slpUtxos.push(thisUtxo); | |||||
} else { | |||||
// Exclude utxos of 546 sats as a precaution against accidentally burning eToken utxos | |||||
// Note: no known case of this being an issue on chronik, but preserve it for now | |||||
if (thisUtxo.value.low === 546) { | |||||
continue; | |||||
} else { | |||||
nonSlpUtxos.push(thisUtxo); | |||||
} | |||||
} | |||||
} | |||||
// Learn how this Long lib works | |||||
for (let i = 0; i < slpUtxos.length; i += 1) { | |||||
const thisSlpUtxo = slpUtxos[i]; | |||||
const tokenQty = new Long(thisSlpUtxo.slpToken.amount).toString(10); | |||||
slpUtxos[i]['tokenQty'] = tokenQty; | |||||
} | |||||
let tokensById = {}; | |||||
slpUtxos.forEach(slpUtxo => { | |||||
let token = tokensById[slpUtxo.slpMeta.tokenId]; | |||||
if (token) { | |||||
if (slpUtxo.tokenQty) { | |||||
token.balance = token.balance.plus( | |||||
new BigNumber(slpUtxo.tokenQty), | |||||
); | |||||
} | |||||
} else { | |||||
token = {}; | |||||
token.tokenId = slpUtxo.slpMeta.tokenId; | |||||
if (slpUtxo.tokenQty) { | |||||
token.balance = new BigNumber(slpUtxo.tokenQty); | |||||
} else { | |||||
token.balance = new BigNumber(0); | |||||
} | |||||
tokensById[slpUtxo.slpMeta.tokenId] = token; | |||||
} | |||||
}); | |||||
const tokens = Object.values(tokensById); | |||||
const chronikSlpBalancesAndUtxos = { slpUtxos, nonSlpUtxos, tokens }; | |||||
return chronikSlpBalancesAndUtxos; | |||||
}; | |||||
const addTokenInfo = async tokens => { | |||||
// for each token, get the genesis info | |||||
// parse token qty by decimal | |||||
const getTokenInfoPromises = []; | |||||
for (let i = 0; i < tokens.length; i += 1) { | |||||
const thisTokenId = tokens[i].tokenId; | |||||
const thisTokenInfoPromise = | |||||
returnGetTokenInfoChronikPromise(thisTokenId); | |||||
getTokenInfoPromises.push(thisTokenInfoPromise); | |||||
} | |||||
let tokenInfoArray = await Promise.all(getTokenInfoPromises); | |||||
// TODO iterate through tokens and add the required tokeninfo | |||||
// note: mb it's better if you organize the token info so it's accessible by tokenId index for this process | |||||
// NB: tokenInfoArray should be in the same order as tokens, by tokenId | |||||
// Do not assume this | |||||
if (tokens.length !== tokenInfoArray.length) { | |||||
console.log( | |||||
`ERROR: tokenInfoArray length is ${tokenInfoArray.length}, while tokens length is ${tokens.length}`, | |||||
); | |||||
} | |||||
for (let i = 0; i < tokens.length; i += 1) { | |||||
const thisToken = tokens[i]; | |||||
const thisTokenId = thisToken.tokenId; | |||||
tokenInfoArrayLoop: for ( | |||||
let j = 0; | |||||
j < tokenInfoArray.length; | |||||
j += 1 | |||||
) { | |||||
const tokenInfoForTokenId = tokenInfoArray[j].tokenId; | |||||
if (thisTokenId === tokenInfoForTokenId) { | |||||
const thisGenesisInfo = tokenInfoArray[j]; | |||||
// Add this info to the utxo | |||||
tokens[i].info = thisGenesisInfo; | |||||
// Adjust the token balance for tokenDecimals | |||||
const tokenDecimals = thisGenesisInfo.decimals; | |||||
// Adjust tokenQty per decimal places | |||||
/* | |||||
console.log( | |||||
`starting balance for ${thisTokenId} with ${tokenDecimals} decimals`, | |||||
tokens[i].balance.toString(), | |||||
); | |||||
*/ | |||||
tokens[i].balance = tokens[i].balance.shiftedBy( | |||||
-1 * tokenDecimals, | |||||
); | |||||
/* | |||||
console.log( | |||||
`ending balance for ${thisTokenId}`, | |||||
tokens[i].balance.toString(), | |||||
); | |||||
*/ | |||||
// you won't need it again, so remove it from tokenInfoArray | |||||
tokenInfoArray.slice(j, 1); | |||||
// do not iterate through the rest of tokenInfoArray once you have found what you are looking for | |||||
break tokenInfoArrayLoop; | |||||
} | |||||
} | |||||
} | |||||
return tokens; | |||||
}; | |||||
return { | return { | ||||
getBCH, | getBCH, | ||||
calcFee, | calcFee, | ||||
getUtxos, | getUtxos, | ||||
getHydratedUtxoDetails, | getHydratedUtxoDetails, | ||||
getSlpBalancesAndUtxos, | getSlpBalancesAndUtxos, | ||||
getTxHistory, | getTxHistory, | ||||
flattenTransactions, | flattenTransactions, | ||||
parseTxData, | parseTxData, | ||||
addTokenTxData, | addTokenTxData, | ||||
parseTokenInfoForTxHistory, | parseTokenInfoForTxHistory, | ||||
getTxData, | getTxData, | ||||
getRestUrl, | getRestUrl, | ||||
signPkMessage, | signPkMessage, | ||||
sendXec, | sendXec, | ||||
sendToken, | sendToken, | ||||
createToken, | createToken, | ||||
getTokenStats, | getTokenStats, | ||||
handleEncryptedOpReturn, | handleEncryptedOpReturn, | ||||
getRecipientPublicKey, | getRecipientPublicKey, | ||||
burnEtoken, | burnEtoken, | ||||
getUtxosChronik, | |||||
getSlpBalancesAndUtxosFromChronik, | |||||
addTokenInfo, | |||||
}; | }; | ||||
} | } |