diff --git a/web/cashtab/src/hooks/__tests__/useBCH.test.js b/web/cashtab/src/hooks/__tests__/useBCH.test.js index c41a8d13f..c1c404210 100644 --- a/web/cashtab/src/hooks/__tests__/useBCH.test.js +++ b/web/cashtab/src/hooks/__tests__/useBCH.test.js @@ -1,370 +1,368 @@ /* eslint-disable no-native-reassign */ import useBCH from '../useBCH'; import sendBCHMock from '../__mocks__/sendBCH'; import createTokenMock from '../__mocks__/createToken'; import { validStoredWallet } from '../../utils/__mocks__/mockStoredWallets'; import BCHJS from '@psf/bch-js'; // TODO: should be removed when external lib not needed anymore import { currency } from '../../components/Common/Ticker'; import BigNumber from 'bignumber.js'; import { fromSatoshisToXec } from 'utils/cashMethods'; import { ChronikClient } from 'chronik-client'; // for mocking purposes describe('useBCH hook', () => { it('gets primary Rest API URL on mainnet', () => { process = { env: { REACT_APP_BCHA_APIS: 'https://rest.kingbch.com/v3/,https://wallet-service-prod.bitframe.org/v3/,notevenaurl,https://rest.kingbch.com/v3/', }, }; const { getRestUrl } = useBCH(); const expectedApiUrl = `https://rest.kingbch.com/v3/`; expect(getRestUrl(0)).toBe(expectedApiUrl); }); it('sends XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const { expectedTxId, utxos, wallet, destinationAddress, sendAmount } = sendBCHMock; chronik.broadcastTx = jest .fn() .mockResolvedValue({ txid: expectedTxId }); expect( await sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); it('sends XEC correctly with an encrypted OP_RETURN message', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const { expectedTxId, utxos, wallet, destinationAddress, sendAmount } = sendBCHMock; const expectedPubKey = '03451a3e61ae8eb76b8d4cd6057e4ebaf3ef63ae3fe5f441b72c743b5810b6a389'; chronik.broadcastTx = jest .fn() .mockResolvedValue({ txid: expectedTxId }); expect( await sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, 'This is an encrypted opreturn message', false, null, destinationAddress, sendAmount, true, // encryption flag for the OP_RETURN message false, // airdrop flag '', // airdrop token id expectedPubKey, //optionalMockPubKeyResponse ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); it('sends one to many XEC correctly', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const { expectedTxId, utxos, wallet } = sendBCHMock; const addressAndValueArray = [ 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6.8', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,7', 'bitcoincash:qrzuvj0vvnsz5949h4axercl5k420eygavv0awgz05,6', ]; chronik.broadcastTx = jest .fn() .mockResolvedValue({ txid: expectedTxId }); expect( await sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', true, addressAndValueArray, ), ).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`); }); it(`Throws error if called trying to send one base unit ${currency.ticker} more than available in utxo set`, async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const { utxos, wallet, destinationAddress } = sendBCHMock; const expectedTxFeeInSats = 229; // tally up the total utxo values let totalInputUtxoValue = new BigNumber(0); for (let i = 0; i < utxos.length; i++) { totalInputUtxoValue = totalInputUtxoValue.plus( new BigNumber(utxos[i].value), ); } const oneBaseUnitMoreThanBalance = totalInputUtxoValue .minus(expectedTxFeeInSats) .plus(1) .div(10 ** currency.cashDecimals) .toString(); let errorThrown; try { await sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, oneBaseUnitMoreThanBalance, ); } catch (err) { errorThrown = err; } expect(errorThrown.message).toStrictEqual('Insufficient funds'); const nullValuesSendBch = sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, null, ); expect(nullValuesSendBch).rejects.toThrow( new Error('Invalid singleSendValue'), ); }); it('Throws error on attempt to send one satoshi less than backend dust limit', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const { utxos, wallet, destinationAddress } = sendBCHMock; const failedSendBch = sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, new BigNumber(fromSatoshisToXec(currency.dustSats).toString()) .minus(new BigNumber('0.00000001')) .toString(), ); expect(failedSendBch).rejects.toThrow(new Error('dust')); }); it("Throws error attempting to burn an eToken ID that is not within the wallet's utxo", async () => { const { burnToken } = useBCH(); const BCH = new BCHJS(); const wallet = validStoredWallet; const burnAmount = 10; const eTokenId = '0203c768a66eba24affNOTVALID103b772de4d9f8f63ba79e'; const expectedError = 'No token UTXOs for the specified token could be found.'; const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); let thrownError; try { await burnToken(BCH, chronik, wallet, { tokenId: eTokenId, amount: burnAmount, }); } catch (err) { thrownError = err; } expect(thrownError).toStrictEqual(new Error(expectedError)); }); it('receives errors from the network and parses it', async () => { const { sendXec } = useBCH(); const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const { sendAmount, utxos, wallet, destinationAddress } = sendBCHMock; chronik.broadcastTx = jest.fn().mockImplementation(async () => { throw new Error('insufficient priority (code 66)'); }); const insufficientPriority = sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(insufficientPriority).rejects.toThrow( new Error('insufficient priority (code 66)'), ); chronik.broadcastTx = jest.fn().mockImplementation(async () => { throw new Error('txn-mempool-conflict (code 18)'); }); const txnMempoolConflict = sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(txnMempoolConflict).rejects.toThrow( new Error('txn-mempool-conflict (code 18)'), ); chronik.broadcastTx = jest.fn().mockImplementation(async () => { throw new Error('Network Error'); }); const networkError = sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(networkError).rejects.toThrow(new Error('Network Error')); chronik.broadcastTx = jest.fn().mockImplementation(async () => { const err = new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ); throw err; }); const tooManyAncestorsMempool = sendXec( BCH, chronik, wallet, utxos, currency.defaultFee, '', false, null, destinationAddress, sendAmount, ); await expect(tooManyAncestorsMempool).rejects.toThrow( new Error( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)', ), ); }); it('creates a token correctly', async () => { const { createToken } = useBCH(); const BCH = new BCHJS(); const { expectedTxId, expectedHex, wallet, configObj } = createTokenMock; const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); chronik.broadcastTx = jest .fn() .mockResolvedValue({ txid: expectedTxId }); expect(await createToken(BCH, chronik, wallet, 5.01, configObj)).toBe( `${currency.blockExplorerUrl}/tx/${expectedTxId}`, ); }); it('Throws correct error if user attempts to create a token with an invalid wallet', async () => { const { createToken } = useBCH(); const BCH = new BCHJS(); const { invalidWallet, configObj } = createTokenMock; const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const invalidWalletTokenCreation = createToken( BCH, chronik, invalidWallet, currency.defaultFee, configObj, ); await expect(invalidWalletTokenCreation).rejects.toThrow( new Error('Invalid wallet'), ); }); it(`getRecipientPublicKey() correctly retrieves the public key of a cash address`, async () => { const { getRecipientPublicKey } = useBCH(); - const BCH = new BCHJS(); const chronik = new ChronikClient( 'https://FakeChronikUrlToEnsureMocksOnly.com', ); const expectedPubKey = '03208c4f52229e021ddec5fc6e07a59fd66388ac52bc2a2c1e0f1afb24b0e275ac'; const destinationAddress = 'bitcoincash:qqvuj09f80sw9j7qru84ptxf0hyqffc38gstxfs5ru'; expect( await getRecipientPublicKey( - BCH, chronik, destinationAddress, expectedPubKey, ), ).toBe(expectedPubKey); }); }); diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js index fadf6a0d3..122cf4eb4 100644 --- a/web/cashtab/src/hooks/useBCH.js +++ b/web/cashtab/src/hooks/useBCH.js @@ -1,537 +1,535 @@ import { currency } from 'components/Common/Ticker'; import SlpWallet from 'minimal-slp-wallet'; import { fromXecToSatoshis, isValidStoredWallet, parseXecSendValue, generateOpReturnScript, generateTxInput, generateTxOutput, generateTokenTxInput, generateTokenTxOutput, signAndBuildTx, getChangeAddressFromInputUtxos, toHash160, } from 'utils/cashMethods'; import ecies from 'ecies-lite'; import TransactionBuilder from 'utils/txBuilder'; export default function useBCH() { const SEND_BCH_ERRORS = { INSUFFICIENT_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_BCHA_APIS; const apiArray = apiString.split(','); return apiArray[apiIndex]; }; const createToken = async ( BCH, chronik, wallet, feeInSatsPerByte, configObj, ) => { try { // Throw error if wallet does not have utxo set in state if (!isValidStoredWallet(wallet)) { const walletError = new Error(`Invalid wallet`); throw walletError; } const utxos = wallet.state.slpBalancesAndUtxos.nonSlpUtxos; const CREATION_ADDR = wallet.Path1899.cashAddress; let txBuilder = new TransactionBuilder(); let tokenTxInputObj = generateTokenTxInput( BCH, 'GENESIS', utxos, null, // total token UTXOS - not applicable for GENESIS tx null, // token ID - not applicable for GENESIS tx null, // token amount - not applicable for GENESIS tx feeInSatsPerByte, txBuilder, ); // update txBuilder object with inputs txBuilder = tokenTxInputObj.txBuilder; let tokenTxOutputObj = generateTokenTxOutput( txBuilder, 'GENESIS', CREATION_ADDR, null, // token UTXOS being spent - not applicable for GENESIS tx tokenTxInputObj.remainderXecValue, configObj, ); // update txBuilder object with outputs txBuilder = tokenTxOutputObj; // sign the collated inputUtxos and build the raw tx hex // returns the raw tx hex string const rawTxHex = signAndBuildTx( tokenTxInputObj.inputXecUtxos, txBuilder, wallet, ); // Broadcast transaction to the network via the chronik client // sample chronik.broadcastTx() response: // {"txid":"0075130c9ecb342b5162bb1a8a870e69c935ea0c9b2353a967cda404401acf19"} let broadcastResponse; try { broadcastResponse = await chronik.broadcastTx( rawTxHex, true, // skipSlpCheck to bypass chronik safety mechanism in place to avoid accidental burns // if the wallet has existing burns via bch-api then chronik will throw 'invalid-slp-burns' errors without this flag ); if (!broadcastResponse) { throw new Error('Empty chronik broadcast response'); } } catch (err) { console.log('Error broadcasting tx to chronik client'); throw err; } // return the explorer link for the broadcasted tx return `${currency.blockExplorerUrl}/tx/${broadcastResponse.txid}`; } 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 sendToken = async ( BCH, chronik, wallet, { tokenId, amount, tokenReceiverAddress }, ) => { const slpBalancesAndUtxos = wallet.state.slpBalancesAndUtxos; const xecUtxos = slpBalancesAndUtxos.nonSlpUtxos; const tokenUtxos = slpBalancesAndUtxos.slpUtxos; const CREATION_ADDR = wallet.Path1899.cashAddress; // Handle error of user having no XEC if ( !slpBalancesAndUtxos || !slpBalancesAndUtxos.nonSlpUtxos || slpBalancesAndUtxos.nonSlpUtxos.length === 0 ) { throw new Error( `You need some ${currency.ticker} to send ${currency.tokenTicker}`, ); } // instance of transaction builder let txBuilder = new TransactionBuilder(); let tokenTxInputObj = generateTokenTxInput( BCH, 'SEND', xecUtxos, tokenUtxos, tokenId, amount, currency.defaultFee, txBuilder, ); // update txBuilder object with inputs txBuilder = tokenTxInputObj.txBuilder; let tokenTxOutputObj = generateTokenTxOutput( txBuilder, 'SEND', CREATION_ADDR, tokenTxInputObj.inputTokenUtxos, tokenTxInputObj.remainderXecValue, null, // token config object - for GENESIS tx only tokenReceiverAddress, amount, ); // update txBuilder object with outputs txBuilder = tokenTxOutputObj; // append the token input UTXOs to the array of XEC input UTXOs for signing const combinedInputUtxos = tokenTxInputObj.inputXecUtxos.concat( tokenTxInputObj.inputTokenUtxos, ); // sign the collated inputUtxos and build the raw tx hex // returns the raw tx hex string const rawTxHex = signAndBuildTx(combinedInputUtxos, txBuilder, wallet); // Broadcast transaction to the network via the chronik client // sample chronik.broadcastTx() response: // {"txid":"0075130c9ecb342b5162bb1a8a870e69c935ea0c9b2353a967cda404401acf19"} let broadcastResponse; try { broadcastResponse = await chronik.broadcastTx( rawTxHex, true, // skipSlpCheck to bypass chronik safety mechanism in place to avoid accidental burns // if the wallet has existing burns via bch-api then chronik will throw 'invalid-slp-burns' errors without this flag ); if (!broadcastResponse) { throw new Error('Empty chronik broadcast response'); } } catch (err) { console.log('Error broadcasting tx to chronik client'); throw err; } // return the explorer link for the broadcasted tx return `${currency.blockExplorerUrl}/tx/${broadcastResponse.txid}`; }; const burnToken = async (BCH, chronik, wallet, { tokenId, amount }) => { const slpBalancesAndUtxos = wallet.state.slpBalancesAndUtxos; const xecUtxos = slpBalancesAndUtxos.nonSlpUtxos; const tokenUtxos = slpBalancesAndUtxos.slpUtxos; const CREATION_ADDR = wallet.Path1899.cashAddress; // Handle error of user having no XEC if ( !slpBalancesAndUtxos || !slpBalancesAndUtxos.nonSlpUtxos || slpBalancesAndUtxos.nonSlpUtxos.length === 0 ) { throw new Error(`You need some ${currency.ticker} to burn eTokens`); } // instance of transaction builder let txBuilder = new TransactionBuilder(); let tokenTxInputObj = generateTokenTxInput( BCH, 'BURN', xecUtxos, tokenUtxos, tokenId, amount, currency.defaultFee, txBuilder, ); // update txBuilder object with inputs txBuilder = tokenTxInputObj.txBuilder; let tokenTxOutputObj = generateTokenTxOutput( txBuilder, 'BURN', CREATION_ADDR, tokenTxInputObj.inputTokenUtxos, tokenTxInputObj.remainderXecValue, null, // token config object - for GENESIS tx only null, // token receiver address - for SEND tx only amount, ); // update txBuilder object with outputs txBuilder = tokenTxOutputObj; // append the token input UTXOs to the array of XEC input UTXOs for signing const combinedInputUtxos = tokenTxInputObj.inputXecUtxos.concat( tokenTxInputObj.inputTokenUtxos, ); // sign the collated inputUtxos and build the raw tx hex // returns the raw tx hex string const rawTxHex = signAndBuildTx(combinedInputUtxos, txBuilder, wallet); // Broadcast transaction to the network via the chronik client // sample chronik.broadcastTx() response: // {"txid":"0075130c9ecb342b5162bb1a8a870e69c935ea0c9b2353a967cda404401acf19"} let broadcastResponse; try { broadcastResponse = await chronik.broadcastTx( rawTxHex, true, // skipSlpCheck to bypass chronik safety mechanism in place to avoid accidental burns ); if (!broadcastResponse) { throw new Error('Empty chronik broadcast response'); } } catch (err) { console.log('Error broadcasting tx to chronik client'); throw err; } // return the explorer link for the broadcasted tx return `${currency.blockExplorerUrl}/tx/${broadcastResponse.txid}`; }; const getRecipientPublicKey = async ( - BCH, chronik, recipientAddress, optionalMockPubKeyResponse = false, ) => { // Necessary because jest can't mock // chronikTxHistoryAtAddress = await chronik.script('p2pkh', recipientAddressHash160).history(/*page=*/ 0, /*page_size=*/ 10); if (optionalMockPubKeyResponse) { return optionalMockPubKeyResponse; } // get hash160 of address let recipientAddressHash160; try { recipientAddressHash160 = toHash160(recipientAddress); } catch (err) { console.log( `Error determining toHash160(${recipientAddress} in getRecipientPublicKey())`, err, ); throw new Error( `Error determining toHash160(${recipientAddress} in getRecipientPublicKey())`, ); } let chronikTxHistoryAtAddress; try { // Get 20 txs. If no outgoing txs in those 20 txs, just don't send the tx chronikTxHistoryAtAddress = await chronik .script('p2pkh', recipientAddressHash160) .history(/*page=*/ 0, /*page_size=*/ 20); } catch (err) { console.log( `Error getting await chronik.script('p2pkh', ${recipientAddressHash160}).history();`, err, ); throw new Error( 'Error fetching tx history to parse for public key', ); } let recipientPubKeyChronik; // Iterate over tx history to find an outgoing tx for (let i = 0; i < chronikTxHistoryAtAddress.txs.length; i += 1) { const { inputs } = chronikTxHistoryAtAddress.txs[i]; for (let j = 0; j < inputs.length; j += 1) { const thisInput = inputs[j]; const thisInputSendingHash160 = thisInput.outputScript; if (thisInputSendingHash160.includes(recipientAddressHash160)) { // Then this is an outgoing tx, you can get the public key from this tx // Get the public key try { recipientPubKeyChronik = chronikTxHistoryAtAddress.txs[i].inputs[ j ].inputScript.slice(-66); } catch (err) { throw new Error( 'Cannot send an encrypted message to a wallet with no outgoing transactions', ); } return recipientPubKeyChronik; } } } // You get here if you find no outgoing txs in the chronik tx history throw new Error( 'Cannot send an encrypted message to a wallet with no outgoing transactions in the last 20 txs', ); }; const sendXec = async ( BCH, chronik, wallet, utxos, feeInSatsPerByte, optionalOpReturnMsg, isOneToMany, destinationAddressAndValueArray, destinationAddress, sendAmount, encryptionFlag, airdropFlag, airdropTokenId, optionalMockPubKeyResponse = false, ) => { try { let txBuilder = new TransactionBuilder(); // parse the input value of XECs to send const value = parseXecSendValue( isOneToMany, sendAmount, destinationAddressAndValueArray, ); const satoshisToSend = fromXecToSatoshis(value); // Throw validation error if fromXecToSatoshis returns false if (!satoshisToSend) { const error = new Error( `Invalid decimal places for send amount`, ); throw error; } let encryptedEj; // serialized encryption data object // if the user has opted to encrypt this message if (encryptionFlag) { try { // get the pub key for the recipient address let recipientPubKey = await getRecipientPublicKey( - BCH, chronik, destinationAddress, optionalMockPubKeyResponse, ); // if the API can't find a pub key, it is due to the wallet having no outbound tx if (recipientPubKey === 'not found') { throw new Error( 'Cannot send an encrypted message to a wallet with no outgoing transactions', ); } // encrypt the message const pubKeyBuf = Buffer.from(recipientPubKey, 'hex'); const bufferedFile = Buffer.from(optionalOpReturnMsg); const structuredEj = await ecies.encrypt( pubKeyBuf, bufferedFile, { compressEpk: true }, ); // Serialize the encrypted data object encryptedEj = Buffer.concat([ structuredEj.epk, structuredEj.iv, structuredEj.ct, structuredEj.mac, ]); } catch (err) { console.log(`sendXec() encryption error.`); throw err; } } // Start of building the OP_RETURN output. // only build the OP_RETURN output if the user supplied it if ( (optionalOpReturnMsg && typeof optionalOpReturnMsg !== 'undefined' && optionalOpReturnMsg.trim() !== '') || airdropFlag ) { const opReturnData = generateOpReturnScript( optionalOpReturnMsg, encryptionFlag, airdropFlag, airdropTokenId, encryptedEj, ); txBuilder.addOutput(opReturnData, 0); } // generate the tx inputs and add to txBuilder instance // returns the updated txBuilder, txFee, totalInputUtxoValue and inputUtxos let txInputObj = generateTxInput( isOneToMany, utxos, txBuilder, destinationAddressAndValueArray, satoshisToSend, feeInSatsPerByte, ); const changeAddress = getChangeAddressFromInputUtxos( txInputObj.inputUtxos, wallet, ); txBuilder = txInputObj.txBuilder; // update the local txBuilder with the generated tx inputs // generate the tx outputs and add to txBuilder instance // returns the updated txBuilder const txOutputObj = generateTxOutput( isOneToMany, value, satoshisToSend, txInputObj.totalInputUtxoValue, destinationAddress, destinationAddressAndValueArray, changeAddress, txInputObj.txFee, txBuilder, ); txBuilder = txOutputObj; // update the local txBuilder with the generated tx outputs // sign the collated inputUtxos and build the raw tx hex // returns the raw tx hex string const rawTxHex = signAndBuildTx( txInputObj.inputUtxos, txBuilder, wallet, ); // Broadcast transaction to the network via the chronik client // sample chronik.broadcastTx() response: // {"txid":"0075130c9ecb342b5162bb1a8a870e69c935ea0c9b2353a967cda404401acf19"} let broadcastResponse; try { broadcastResponse = await chronik.broadcastTx(rawTxHex); if (!broadcastResponse) { throw new Error('Empty chronik broadcast response'); } } catch (err) { console.log('Error broadcasting tx to chronik client'); throw err; } // return the explorer link for the broadcasted tx return `${currency.blockExplorerUrl}/tx/${broadcastResponse.txid}`; } 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) => { let ConstructedSlpWallet; ConstructedSlpWallet = new SlpWallet('', { restURL: getRestUrl(apiIndex), }); return ConstructedSlpWallet.bchjs; }; return { getBCH, getRestUrl, sendXec, sendToken, createToken, getRecipientPublicKey, burnToken, }; }