diff --git a/cashtab/src/components/Alias/Alias.js b/cashtab/src/components/Alias/Alias.js
index d45f56a40..ed427cfe5 100644
--- a/cashtab/src/components/Alias/Alias.js
+++ b/cashtab/src/components/Alias/Alias.js
@@ -1,436 +1,445 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { WalletContext } from 'utils/context';
import PropTypes from 'prop-types';
import WalletLabel from 'components/Common/WalletLabel.js';
import {
ZeroBalanceHeader,
SidePaddingCtn,
WalletInfoCtn,
} from 'components/Common/Atoms';
import { DestinationAddressSingle } from 'components/Common/EnhancedInputs';
import { AntdFormWrapper } from 'components/Common/EnhancedInputs';
import { Form, Modal } from 'antd';
import { SmartButton } from 'components/Common/PrimaryButton';
import BalanceHeader from 'components/Common/BalanceHeader';
import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat';
import { Row, Col } from 'antd';
import { UserOutlined } from '@ant-design/icons';
import {
getWalletState,
fromSatoshisToXec,
getAliasRegistrationFee,
convertToEcashPrefix,
getAliasByteSize,
} from 'utils/cashMethods';
import { isAliasAvailable, isAddressRegistered } from 'utils/chronik';
import { currency } from 'components/Common/Ticker.js';
import { registerNewAlias } from 'utils/transactions';
import {
errorNotification,
registerAliasNotification,
} from 'components/Common/Notifications';
import { isAliasFormat, isValidAliasString } from 'utils/validation';
import { getPendingAliases } from 'utils/aliasUtils';
export const NamespaceCtn = styled.div`
width: 100%;
margin-top: 50px;
margin-bottom: 20px;
overflow-wrap: break-word;
h2 {
color: ${props => props.theme.contrast};
margin: 0 0 20px;
}
h3 {
color: ${props => props.theme.contrast};
margin: 0 0 10px;
}
white-space: pre-wrap;
`;
const StyledSpacer = styled.div`
height: 1px;
width: 100%;
background-color: ${props => props.theme.lightWhite};
margin: 60px 0 50px;
`;
const Alias = ({ passLoadingStatus }) => {
const ContextValue = React.useContext(WalletContext);
const {
wallet,
fiatPrice,
cashtabSettings,
chronik,
changeCashtabSettings,
cashtabCache,
isAliasServerOnline,
} = ContextValue;
const walletState = getWalletState(wallet);
- const { balances, nonSlpUtxos } = walletState;
+ const { balances } = walletState;
const [formData, setFormData] = useState({
aliasName: '',
+ aliasAddress: '',
});
const [isValidAliasInput, setIsValidAliasInput] = useState(false); // tracks whether to activate the registration button
const [aliasValidationError, setAliasValidationError] = useState(false);
const [activeWalletAliases, setActiveWalletAliases] = useState([]); // stores the list of aliases registered to this active wallet
const [aliasLength, setAliasLength] = useState(false); // real time tracking of alias char length
const [aliasFee, setAliasFee] = useState(false); // real time tracking of alias registration fee
// Show a confirmation modal on alias registrations
const [isModalVisible, setIsModalVisible] = useState(false);
useEffect(() => {
passLoadingStatus(false);
}, [balances.totalBalance]);
useEffect(async () => {
// only run this useEffect block if wallet or cashtabCache is defined
if (
!wallet ||
typeof wallet === 'undefined' ||
!cashtabCache ||
typeof cashtabCache === 'undefined'
) {
return;
}
passLoadingStatus(true);
+ // Set address of active wallet to default alias registration address
+ // Use formdata approach as we will later add a form field for aliasAddress
+ setFormData(formData => ({
+ ...formData,
+ aliasAddress: wallet.Path1899.cashAddress,
+ }));
+
// check whether the address is attached to an onchain alias on page load
const walletHasAlias = isAddressRegistered(
wallet,
cashtabCache.aliasCache,
);
// retrieve aliases for this active wallet from cache for rendering on the frontend
if (
walletHasAlias &&
cashtabCache.aliasCache &&
cashtabCache.aliasCache.cachedAliasCount > 0
) {
const thisAddress = convertToEcashPrefix(
wallet.Path1899.cashAddress,
);
// filter for aliases that matches this wallet's address
const registeredAliasesToWallet =
cashtabCache.aliasCache.aliases.filter(
alias => alias.address === thisAddress,
);
const appendedWithPendingAliases = await appendWithPendingAliases(
registeredAliasesToWallet,
);
setActiveWalletAliases([...new Set(appendedWithPendingAliases)]); // new Set() removes duplicate entries
}
passLoadingStatus(false);
}, [wallet.name, cashtabCache.aliasCache.aliases]);
const handleOk = () => {
setIsModalVisible(false);
registerAlias();
};
const handleCancel = () => {
setIsModalVisible(false);
};
const registerAlias = async () => {
passLoadingStatus(true);
if (!isAliasServerOnline) {
// error notification on alias-server being unavailable
errorNotification(
null,
'Unable to connect to alias server, please try again later',
'Alias-server status check',
);
passLoadingStatus(false);
return;
}
// note: input already validated via handleAliasNameInput()
const aliasInput = formData.aliasName;
+ const aliasAddress = formData.aliasAddress;
// check if the user is trying to essentially register chicken.xec.xec
const doubleExtensionInput = isAliasFormat(aliasInput);
if (doubleExtensionInput) {
errorNotification(
null,
'Please input an alias without the ".xec"',
'Alias extension check',
);
passLoadingStatus(false);
return;
}
const aliasAvailable = await isAliasAvailable(
aliasInput,
cashtabCache.aliasCache,
);
if (aliasAvailable) {
// calculate registration fee based on chars
const registrationFee = getAliasRegistrationFee(aliasInput);
console.log(
'Registration fee for ' +
aliasInput +
' is ' +
registrationFee +
' sats.',
);
console.log(
`Alias ${aliasInput} is available. Broadcasting registration transaction.`,
);
try {
- const link = await registerNewAlias(
+ const result = await registerNewAlias(
chronik,
wallet,
- nonSlpUtxos,
currency.defaultFee,
aliasInput,
- fromSatoshisToXec(registrationFee),
+ aliasAddress,
+ registrationFee,
);
- registerAliasNotification(link, aliasInput);
+ registerAliasNotification(result.explorerLink, aliasInput);
// allow alias server to process the pending alias
const delay = ms => new Promise(res => setTimeout(res, ms));
await delay(1000); // 1 second
const appendedWithPendingAliases =
await appendWithPendingAliases(activeWalletAliases);
setActiveWalletAliases([
...new Set(appendedWithPendingAliases),
]); // new Set() removes duplicate entries
} catch (err) {
handleAliasRegistrationError(err);
}
setIsValidAliasInput(true);
} else {
// error notification on alias being unavailable
errorNotification(
null,
'This alias [' +
aliasInput +
'] has already been taken, please try another alias',
'Alias availability check',
);
}
passLoadingStatus(false);
};
const appendWithPendingAliases = async currentActiveWalletAliases => {
// retrieve the pending aliases and add to the registered aliases list for this wallet
let pendingAliasesArray = await getPendingAliases();
if (!pendingAliasesArray) {
return currentActiveWalletAliases;
}
// append the pending indicator
for (let i = 0; i < pendingAliasesArray.length; i += 1) {
pendingAliasesArray[i].alias =
pendingAliasesArray[i].alias + ' (pending)';
}
// filter to pending aliases matching this active wallet
const thisAddress = convertToEcashPrefix(wallet.Path1899.cashAddress);
const pendingAndConfirmedAliases = pendingAliasesArray.filter(
element => element.address === thisAddress,
);
// merge pending with confirmed list
let tempActiveWalletAliases = currentActiveWalletAliases;
tempActiveWalletAliases = tempActiveWalletAliases.concat(
pendingAndConfirmedAliases,
);
return tempActiveWalletAliases;
};
const handleAliasNameInput = e => {
const { name, value } = e.target;
const validAliasInput = isValidAliasString(value);
const aliasInputByteSize = getAliasByteSize(value);
if (
value &&
value.trim() !== '' &&
aliasInputByteSize <= currency.aliasSettings.aliasMaxLength &&
validAliasInput
) {
setIsValidAliasInput(true);
const registrationFee = getAliasRegistrationFee(value);
setAliasFee(registrationFee);
setAliasLength(aliasInputByteSize);
setAliasValidationError(false);
} else {
setAliasValidationError(
'Please enter an alias (lowercase a-z, 0-9) between 1 and 21 bytes',
);
setIsValidAliasInput(false);
setAliasFee(false);
setAliasLength(false);
}
setFormData(p => ({
...p,
[name]: value,
}));
};
function handleAliasRegistrationError(errorObj) {
// Set loading to false here as well, as balance may not change depending on where error occured in try loop
passLoadingStatus(false);
let message;
if (
errorObj.error &&
errorObj.error.includes(
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)',
)
) {
message = `The address you are trying to register has too many unconfirmed ancestors (limit 50). Registration will be possible after a block confirmation. Try again in about 10 minutes.`;
} else {
message =
errorObj.message || errorObj.error || JSON.stringify(errorObj);
}
errorNotification(errorObj, message, 'Registering Alias');
}
return (
<>
{`Are you sure you want to register the alias '${
formData.aliasName
}' for ${fromSatoshisToXec(aliasFee)} XECs?`}
{!balances.totalBalance ? (
You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : (
<>
{fiatPrice !== null && (
)}
>
)}
eCash Namespace Alias
handleAliasNameInput(e),
required: true,
}}
/>
{aliasLength &&
aliasFee &&
`Registration fee for this ${aliasLength} byte Alias is ${fromSatoshisToXec(
aliasFee,
)} XEC`}
setIsModalVisible(true)
}
>
Register Alias
Registered aliases
{activeWalletAliases &&
activeWalletAliases.length > 0
? activeWalletAliases
.map(
alias => alias.alias + '.xec',
)
.join('\n')
: 'N/A'}
>
);
};
/*
passLoadingStatus must receive a default prop that is a function
in order to pass the rendering unit test in Alias.test.js
status => {console.log(status)} is an arbitrary stub function
*/
Alias.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
Alias.propTypes = {
passLoadingStatus: PropTypes.func,
};
export default Alias;
diff --git a/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
index f69ed82c7..06b57691b 100644
--- a/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
+++ b/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap
@@ -1,2952 +1,2952 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances 1`] = `
Array [
MigrationTestAlpha
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Wallet with BCH balances and tokens 1`] = `
Array [
MigrationTestAlpha
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
Array [
You currently have 0
XEC
Deposit some funds to use this feature
,
,
]
`;
diff --git a/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
index 8876d562c..087e41fc1 100644
--- a/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
+++ b/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap
@@ -1,255 +1,255 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances and tokens 1`] = `null`;
exports[`Wallet with BCH balances and tokens and state field 1`] = `
6.001000000
TBS
`;
exports[`Without wallet defined 1`] = `null`;
diff --git a/cashtab/src/utils/__mocks__/registerNewAliasMocks.js b/cashtab/src/utils/__mocks__/registerNewAliasMocks.js
new file mode 100644
index 000000000..37ff8191e
--- /dev/null
+++ b/cashtab/src/utils/__mocks__/registerNewAliasMocks.js
@@ -0,0 +1,608 @@
+// @generated
+
+export const aliasRegisteringWallet = {
+ mnemonic:
+ 'cheese purpose sphere member afford orbit aisle bundle shoe save delay poverty',
+ name: '[burned]aliasRegTest',
+ Path245: {
+ publicKey:
+ '033a8335572ad5877fb4691d702dc276e965e3dfc61b7ddb1266e56ee9e63b07c7',
+ hash160: '260ad4f88031bf90191db87956a141a4caee41b9',
+ cashAddress: 'ecash:qqnq448csqcmlyqerku8j44pgxjv4mjphyvf4q7ku5',
+ fundingWif: 'L2z9MWRYBdajS1JUvry8ef3H4dMoJiQpL7H9gh9QY9uN4tTW5sty',
+ },
+ Path145: {
+ publicKey:
+ '03bb8d0f9b479bbbf17025f1c58c34c9831ad4ac16f1707fb3e19bf4a990eed517',
+ hash160: '7774bdf6dc5f88dcbafc9876ddda68d6ef9f5079',
+ cashAddress: 'ecash:qpmhf00km30c3h96ljv8dhw6drtwl86s0ysn32vngq',
+ fundingWif: 'Kwhw5NivkPBigRCsRFeab2W62yWHH3aNAK8PBAeK4cW13Sk3NM98',
+ },
+ Path1899: {
+ publicKey:
+ '036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6',
+ hash160: '20edc8389101aed204b9c17b7d64a00ead0e8cfc',
+ cashAddress: 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ fundingWif: 'L3NFW2e1T92cF6YZydoEtpmYSm1hhmUzJ9fXgBAzF8ts4Eugno2D',
+ },
+ state: {
+ balances: {
+ totalBalanceInSatoshis: '10000000',
+ totalBalance: '100000',
+ },
+ slpUtxos: [],
+ nonSlpUtxos: [
+ {
+ outpoint: {
+ txid: '879e64ba90c09d957f6b7043776cd5c171ee2c7c82e728bd1fe97899f42d9c04',
+ outIdx: 0,
+ },
+ blockHeight: 790294,
+ isCoinbase: false,
+ value: '10000000',
+ network: 'XEC',
+ address: 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ },
+ ],
+ tokens: [],
+ parsedTxHistory: [
+ {
+ txid: '879e64ba90c09d957f6b7043776cd5c171ee2c7c82e728bd1fe97899f42d9c04',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '88fec8beeda59c72b06b6d5d9531a03e4ecb46a0097501565361900f9dcc280a',
+ outIdx: 1,
+ },
+ inputScript:
+ '47304402206bb74db30b624d62b51e00f1f4a309ad7c037c55c45ac38425a63e4b168e92ad022034e464b5f6136bb63345a32e40aa0c3224fcfb6ad4bb7449094b84f36a331e294121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '8849259',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '1c0b5e632601d041a4423e05aa9824c8d368eb01c8a9aaefc506233879349404',
+ outIdx: 3,
+ },
+ inputScript:
+ '4730440220097633057fd28fb098c11012e63e05850b4f172f37dd58373c14ef80fdc790cb02203f0d5f44b92f3e659c605faac2c6743eacf223f695d1ce7cfd07796ff0a241694121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '5000',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '179f65e21f4bb3ee3bf9d0433b7a6df7ed6e5a795b2c1aea47902e1fb0e39fe3',
+ outIdx: 1,
+ },
+ inputScript:
+ '473044022062f6dcddca0d477dedd0024cabf0b4e22f2b213231f385e0b68e9e5f41eb04e602201597c7df3495bab6093612d2628b8c30554c27a24d452bc11e3a51fd37727f264121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '1100674',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '9da7445bb061944175710efcb5a22118aceb6c23c8bc82d4afeddddd410ea2b5',
+ outIdx: 2,
+ },
+ inputScript:
+ '47304402207d591fe5f8f6d71e120634be09674e36074122c3291c00c18e6807397ecac323022021e7e9c2c0fb724e99a74e0dd5dba453d292a1cd32f55e18a33fcf515b50e9784121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '9998848',
+ sequenceNo: 4294967295,
+ },
+ ],
+ outputs: [
+ {
+ value: '10000000',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ },
+ {
+ value: '9952434',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ },
+ ],
+ lockTime: 0,
+ block: {
+ height: 790294,
+ hash: '0000000000000000084fe051052cbbe8778135f254076b3e20e9f08387654b2f',
+ timestamp: '1683047794',
+ },
+ timeFirstSeen: '1683047404',
+ size: 666,
+ isCoinbase: false,
+ network: 'XEC',
+ parsed: {
+ incoming: true,
+ xecAmount: '100000',
+ isEtokenTx: false,
+ airdropFlag: false,
+ airdropTokenId: '',
+ opReturnMessage: '',
+ isCashtabMessage: false,
+ isEncryptedMessage: false,
+ decryptionSuccess: false,
+ replyAddress:
+ 'ecash:qplkmuz3rx480u6vc4xgc0qxnza42p0e7vll6p90wr',
+ aliasFlag: false,
+ },
+ },
+ ],
+ },
+};
+export const aliasRegisteringWalletAfterTx = {
+ mnemonic:
+ 'cheese purpose sphere member afford orbit aisle bundle shoe save delay poverty',
+ name: '[burned]aliasRegTest',
+ Path245: {
+ publicKey:
+ '033a8335572ad5877fb4691d702dc276e965e3dfc61b7ddb1266e56ee9e63b07c7',
+ hash160: '260ad4f88031bf90191db87956a141a4caee41b9',
+ cashAddress: 'ecash:qqnq448csqcmlyqerku8j44pgxjv4mjphyvf4q7ku5',
+ fundingWif: 'L2z9MWRYBdajS1JUvry8ef3H4dMoJiQpL7H9gh9QY9uN4tTW5sty',
+ },
+ Path145: {
+ publicKey:
+ '03bb8d0f9b479bbbf17025f1c58c34c9831ad4ac16f1707fb3e19bf4a990eed517',
+ hash160: '7774bdf6dc5f88dcbafc9876ddda68d6ef9f5079',
+ cashAddress: 'ecash:qpmhf00km30c3h96ljv8dhw6drtwl86s0ysn32vngq',
+ fundingWif: 'Kwhw5NivkPBigRCsRFeab2W62yWHH3aNAK8PBAeK4cW13Sk3NM98',
+ },
+ Path1899: {
+ publicKey:
+ '036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6',
+ hash160: '20edc8389101aed204b9c17b7d64a00ead0e8cfc',
+ cashAddress: 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ fundingWif: 'L3NFW2e1T92cF6YZydoEtpmYSm1hhmUzJ9fXgBAzF8ts4Eugno2D',
+ },
+ state: {
+ balances: {
+ totalBalanceInSatoshis: '9998994',
+ totalBalance: '99989.94',
+ },
+ slpUtxos: [],
+ nonSlpUtxos: [
+ {
+ outpoint: {
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ outIdx: 2,
+ },
+ blockHeight: 790297,
+ isCoinbase: false,
+ value: '9998994',
+ network: 'XEC',
+ address: 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ },
+ ],
+ tokens: [],
+ parsedTxHistory: [
+ {
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '879e64ba90c09d957f6b7043776cd5c171ee2c7c82e728bd1fe97899f42d9c04',
+ outIdx: 0,
+ },
+ inputScript:
+ '483045022100dd79c2a3e8773e7d4963ad3f6f03b54aca569e8e383c5cdc285afa601ee50829022058dfcdc905ec8f45d7418644ccfdf45cd8db41aade2d57d455c50e1a181cf7bb4121036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ value: '10000000',
+ sequenceNo: 4294967295,
+ },
+ ],
+ outputs: [
+ {
+ value: '0',
+ outputScript:
+ '6a042e78656300086e65777465737431150020edc8389101aed204b9c17b7d64a00ead0e8cfc',
+ },
+ {
+ value: '551',
+ outputScript:
+ '76a914638568e36d0b5d7d49a6e99854caa27d9772b09388ac',
+ },
+ {
+ value: '9998994',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ },
+ ],
+ lockTime: 0,
+ block: {
+ height: 790297,
+ hash: '000000000000000016920644c8c4abcf1681a6ca9068e532410554e901848d41',
+ timestamp: '1683049955',
+ },
+ timeFirstSeen: '1683049080',
+ size: 273,
+ isCoinbase: false,
+ network: 'XEC',
+ parsed: {
+ incoming: false,
+ xecAmount: '5.51',
+ isEtokenTx: false,
+ airdropFlag: false,
+ airdropTokenId: '',
+ opReturnMessage: '',
+ isCashtabMessage: false,
+ isEncryptedMessage: false,
+ decryptionSuccess: false,
+ replyAddress:
+ 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ aliasFlag: true,
+ },
+ },
+ {
+ txid: '879e64ba90c09d957f6b7043776cd5c171ee2c7c82e728bd1fe97899f42d9c04',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '88fec8beeda59c72b06b6d5d9531a03e4ecb46a0097501565361900f9dcc280a',
+ outIdx: 1,
+ },
+ inputScript:
+ '47304402206bb74db30b624d62b51e00f1f4a309ad7c037c55c45ac38425a63e4b168e92ad022034e464b5f6136bb63345a32e40aa0c3224fcfb6ad4bb7449094b84f36a331e294121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '8849259',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '1c0b5e632601d041a4423e05aa9824c8d368eb01c8a9aaefc506233879349404',
+ outIdx: 3,
+ },
+ inputScript:
+ '4730440220097633057fd28fb098c11012e63e05850b4f172f37dd58373c14ef80fdc790cb02203f0d5f44b92f3e659c605faac2c6743eacf223f695d1ce7cfd07796ff0a241694121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '5000',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '179f65e21f4bb3ee3bf9d0433b7a6df7ed6e5a795b2c1aea47902e1fb0e39fe3',
+ outIdx: 1,
+ },
+ inputScript:
+ '473044022062f6dcddca0d477dedd0024cabf0b4e22f2b213231f385e0b68e9e5f41eb04e602201597c7df3495bab6093612d2628b8c30554c27a24d452bc11e3a51fd37727f264121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '1100674',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '9da7445bb061944175710efcb5a22118aceb6c23c8bc82d4afeddddd410ea2b5',
+ outIdx: 2,
+ },
+ inputScript:
+ '47304402207d591fe5f8f6d71e120634be09674e36074122c3291c00c18e6807397ecac323022021e7e9c2c0fb724e99a74e0dd5dba453d292a1cd32f55e18a33fcf515b50e9784121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '9998848',
+ sequenceNo: 4294967295,
+ },
+ ],
+ outputs: [
+ {
+ value: '10000000',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ spentBy: {
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ outIdx: 0,
+ },
+ },
+ {
+ value: '9952434',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ },
+ ],
+ lockTime: 0,
+ block: {
+ height: 790294,
+ hash: '0000000000000000084fe051052cbbe8778135f254076b3e20e9f08387654b2f',
+ timestamp: '1683047794',
+ },
+ timeFirstSeen: '1683047404',
+ size: 666,
+ isCoinbase: false,
+ network: 'XEC',
+ parsed: {
+ incoming: true,
+ xecAmount: '100000',
+ isEtokenTx: false,
+ airdropFlag: false,
+ airdropTokenId: '',
+ opReturnMessage: '',
+ isCashtabMessage: false,
+ isEncryptedMessage: false,
+ decryptionSuccess: false,
+ replyAddress:
+ 'ecash:qplkmuz3rx480u6vc4xgc0qxnza42p0e7vll6p90wr',
+ aliasFlag: false,
+ },
+ },
+ ],
+ },
+};
+export const aliasRegisteringWalletAfterTwoTxs = {
+ mnemonic:
+ 'cheese purpose sphere member afford orbit aisle bundle shoe save delay poverty',
+ name: '[burned]aliasRegTest',
+ Path245: {
+ publicKey:
+ '033a8335572ad5877fb4691d702dc276e965e3dfc61b7ddb1266e56ee9e63b07c7',
+ hash160: '260ad4f88031bf90191db87956a141a4caee41b9',
+ cashAddress: 'ecash:qqnq448csqcmlyqerku8j44pgxjv4mjphyvf4q7ku5',
+ fundingWif: 'L2z9MWRYBdajS1JUvry8ef3H4dMoJiQpL7H9gh9QY9uN4tTW5sty',
+ },
+ Path145: {
+ publicKey:
+ '03bb8d0f9b479bbbf17025f1c58c34c9831ad4ac16f1707fb3e19bf4a990eed517',
+ hash160: '7774bdf6dc5f88dcbafc9876ddda68d6ef9f5079',
+ cashAddress: 'ecash:qpmhf00km30c3h96ljv8dhw6drtwl86s0ysn32vngq',
+ fundingWif: 'Kwhw5NivkPBigRCsRFeab2W62yWHH3aNAK8PBAeK4cW13Sk3NM98',
+ },
+ Path1899: {
+ publicKey:
+ '036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6',
+ hash160: '20edc8389101aed204b9c17b7d64a00ead0e8cfc',
+ cashAddress: 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ fundingWif: 'L3NFW2e1T92cF6YZydoEtpmYSm1hhmUzJ9fXgBAzF8ts4Eugno2D',
+ },
+ state: {
+ balances: {
+ totalBalanceInSatoshis: '9997988',
+ totalBalance: '99979.88',
+ },
+ slpUtxos: [],
+ nonSlpUtxos: [
+ {
+ outpoint: {
+ txid: '912582a1dc11b568f14f8ebae15cbb0ce53bdb973e137e7dc7c9b261327e6cab',
+ outIdx: 2,
+ },
+ blockHeight: 790312,
+ isCoinbase: false,
+ value: '9997988',
+ network: 'XEC',
+ address: 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ },
+ ],
+ tokens: [],
+ parsedTxHistory: [
+ {
+ txid: '912582a1dc11b568f14f8ebae15cbb0ce53bdb973e137e7dc7c9b261327e6cab',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ outIdx: 2,
+ },
+ inputScript:
+ '47304402205cf1941bd0ed8c49319189973feebd14e8d7faf6c5a3cdc6c16f676bd62c63ac022068c10726326e37f960433a92894609f7f0b65946d8104038110e2104d973c8e24121036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ value: '9998994',
+ sequenceNo: 4294967295,
+ },
+ ],
+ outputs: [
+ {
+ value: '0',
+ outputScript:
+ '6a042e78656300157477656e74796f6e6562797465616c696173726567150020edc8389101aed204b9c17b7d64a00ead0e8cfc',
+ },
+ {
+ value: '551',
+ outputScript:
+ '76a914638568e36d0b5d7d49a6e99854caa27d9772b09388ac',
+ },
+ {
+ value: '9997988',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ },
+ ],
+ lockTime: 0,
+ block: {
+ height: 790312,
+ hash: '0000000000000000170185a5ef34ce2d9eaa9eebc540dadac15eb154c8c7aedd',
+ timestamp: '1683063775',
+ },
+ timeFirstSeen: '1683063315',
+ size: 285,
+ isCoinbase: false,
+ network: 'XEC',
+ parsed: {
+ incoming: false,
+ xecAmount: '5.51',
+ isEtokenTx: false,
+ airdropFlag: false,
+ airdropTokenId: '',
+ opReturnMessage: '',
+ isCashtabMessage: false,
+ isEncryptedMessage: false,
+ decryptionSuccess: false,
+ replyAddress:
+ 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ aliasFlag: true,
+ },
+ },
+ {
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '879e64ba90c09d957f6b7043776cd5c171ee2c7c82e728bd1fe97899f42d9c04',
+ outIdx: 0,
+ },
+ inputScript:
+ '483045022100dd79c2a3e8773e7d4963ad3f6f03b54aca569e8e383c5cdc285afa601ee50829022058dfcdc905ec8f45d7418644ccfdf45cd8db41aade2d57d455c50e1a181cf7bb4121036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ value: '10000000',
+ sequenceNo: 4294967295,
+ },
+ ],
+ outputs: [
+ {
+ value: '0',
+ outputScript:
+ '6a042e78656300086e65777465737431150020edc8389101aed204b9c17b7d64a00ead0e8cfc',
+ },
+ {
+ value: '551',
+ outputScript:
+ '76a914638568e36d0b5d7d49a6e99854caa27d9772b09388ac',
+ },
+ {
+ value: '9998994',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ spentBy: {
+ txid: '912582a1dc11b568f14f8ebae15cbb0ce53bdb973e137e7dc7c9b261327e6cab',
+ outIdx: 0,
+ },
+ },
+ ],
+ lockTime: 0,
+ block: {
+ height: 790297,
+ hash: '000000000000000016920644c8c4abcf1681a6ca9068e532410554e901848d41',
+ timestamp: '1683049955',
+ },
+ timeFirstSeen: '1683049080',
+ size: 273,
+ isCoinbase: false,
+ network: 'XEC',
+ parsed: {
+ incoming: false,
+ xecAmount: '5.51',
+ isEtokenTx: false,
+ airdropFlag: false,
+ airdropTokenId: '',
+ opReturnMessage: '',
+ isCashtabMessage: false,
+ isEncryptedMessage: false,
+ decryptionSuccess: false,
+ replyAddress:
+ 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ aliasFlag: true,
+ },
+ },
+ {
+ txid: '879e64ba90c09d957f6b7043776cd5c171ee2c7c82e728bd1fe97899f42d9c04',
+ version: 2,
+ inputs: [
+ {
+ prevOut: {
+ txid: '88fec8beeda59c72b06b6d5d9531a03e4ecb46a0097501565361900f9dcc280a',
+ outIdx: 1,
+ },
+ inputScript:
+ '47304402206bb74db30b624d62b51e00f1f4a309ad7c037c55c45ac38425a63e4b168e92ad022034e464b5f6136bb63345a32e40aa0c3224fcfb6ad4bb7449094b84f36a331e294121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '8849259',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '1c0b5e632601d041a4423e05aa9824c8d368eb01c8a9aaefc506233879349404',
+ outIdx: 3,
+ },
+ inputScript:
+ '4730440220097633057fd28fb098c11012e63e05850b4f172f37dd58373c14ef80fdc790cb02203f0d5f44b92f3e659c605faac2c6743eacf223f695d1ce7cfd07796ff0a241694121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '5000',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '179f65e21f4bb3ee3bf9d0433b7a6df7ed6e5a795b2c1aea47902e1fb0e39fe3',
+ outIdx: 1,
+ },
+ inputScript:
+ '473044022062f6dcddca0d477dedd0024cabf0b4e22f2b213231f385e0b68e9e5f41eb04e602201597c7df3495bab6093612d2628b8c30554c27a24d452bc11e3a51fd37727f264121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '1100674',
+ sequenceNo: 4294967295,
+ },
+ {
+ prevOut: {
+ txid: '9da7445bb061944175710efcb5a22118aceb6c23c8bc82d4afeddddd410ea2b5',
+ outIdx: 2,
+ },
+ inputScript:
+ '47304402207d591fe5f8f6d71e120634be09674e36074122c3291c00c18e6807397ecac323022021e7e9c2c0fb724e99a74e0dd5dba453d292a1cd32f55e18a33fcf515b50e9784121020d867a730bb6b55078d4eaf36fbf06d2a3867f81c20ad54925bcdd02ec914cbb',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ value: '9998848',
+ sequenceNo: 4294967295,
+ },
+ ],
+ outputs: [
+ {
+ value: '10000000',
+ outputScript:
+ '76a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac',
+ spentBy: {
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ outIdx: 0,
+ },
+ },
+ {
+ value: '9952434',
+ outputScript:
+ '76a9147f6df05119aa77f34cc54c8c3c0698bb5505f9f388ac',
+ },
+ ],
+ lockTime: 0,
+ block: {
+ height: 790294,
+ hash: '0000000000000000084fe051052cbbe8778135f254076b3e20e9f08387654b2f',
+ timestamp: '1683047794',
+ },
+ timeFirstSeen: '1683047404',
+ size: 666,
+ isCoinbase: false,
+ network: 'XEC',
+ parsed: {
+ incoming: true,
+ xecAmount: '100000',
+ isEtokenTx: false,
+ airdropFlag: false,
+ airdropTokenId: '',
+ opReturnMessage: '',
+ isCashtabMessage: false,
+ isEncryptedMessage: false,
+ decryptionSuccess: false,
+ replyAddress:
+ 'ecash:qplkmuz3rx480u6vc4xgc0qxnza42p0e7vll6p90wr',
+ aliasFlag: false,
+ },
+ },
+ ],
+ },
+};
diff --git a/cashtab/src/utils/__tests__/transactions.test.js b/cashtab/src/utils/__tests__/transactions.test.js
index 518ead698..8f8dbd828 100644
--- a/cashtab/src/utils/__tests__/transactions.test.js
+++ b/cashtab/src/utils/__tests__/transactions.test.js
@@ -1,330 +1,419 @@
/* eslint-disable no-native-reassign */
import sendBCHMock from '../__mocks__/sendBCH';
+import {
+ aliasRegisteringWallet,
+ aliasRegisteringWalletAfterTx,
+ aliasRegisteringWalletAfterTwoTxs,
+} from '../__mocks__/registerNewAliasMocks';
import createTokenMock from '../__mocks__/createToken';
import { burnTokenWallet } from '../__mocks__/burnToken';
import { currency } from '../../components/Common/Ticker';
import BigNumber from 'bignumber.js';
import { fromSatoshisToXec } from 'utils/cashMethods';
import { ChronikClient } from 'chronik-client'; // for mocking purposes
import {
sendXec,
burnToken,
createToken,
getRecipientPublicKey,
+ registerNewAlias,
} from 'utils/transactions';
describe('Cashtab transaction broadcasting functions', () => {
it('sends XEC correctly', async () => {
const chronik = new ChronikClient(
'https://FakeChronikUrlToEnsureMocksOnly.com',
);
const { expectedTxId, utxos, wallet, destinationAddress, sendAmount } =
sendBCHMock;
chronik.broadcastTx = jest
.fn()
.mockResolvedValue({ txid: expectedTxId });
expect(
await sendXec(
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 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(
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 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(
chronik,
wallet,
utxos,
currency.defaultFee,
'',
true,
addressAndValueArray,
),
).toBe(`${currency.blockExplorerUrl}/tx/${expectedTxId}`);
});
+ it('Broadcasts a v0 alias registration tx for an 8-byte alias to a p2pkh address', async () => {
+ const chronik = new ChronikClient(
+ 'https://FakeChronikUrlToEnsureMocksOnly.com',
+ );
+
+ const mockTxid =
+ '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0';
+
+ chronik.broadcastTx = jest.fn().mockResolvedValue({ txid: mockTxid });
+
+ const expectedResult = {
+ explorerLink:
+ 'https://explorer.e.cash/tx/1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ rawTxHex:
+ '0200000001049c2df49978e91fbd28e7827c2cee71c1d56c7743706b7f959dc090ba649e87000000006b483045022100dd79c2a3e8773e7d4963ad3f6f03b54aca569e8e383c5cdc285afa601ee50829022058dfcdc905ec8f45d7418644ccfdf45cd8db41aade2d57d455c50e1a181cf7bb4121036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6ffffffff030000000000000000266a042e78656300086e65777465737431150020edc8389101aed204b9c17b7d64a00ead0e8cfc27020000000000001976a914638568e36d0b5d7d49a6e99854caa27d9772b09388ac92929800000000001976a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac00000000',
+ txid: '1272c4a9bf5829c9dba1efb252e753ed20e3cdd49b6e75a778befc7a87eaf7d0',
+ };
+ expect(
+ await registerNewAlias(
+ chronik,
+ aliasRegisteringWallet,
+ currency.defaultFee,
+ 'newtest1',
+ 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ 551,
+ ),
+ ).toStrictEqual(expectedResult);
+ });
+ it('Broadcasts a v0 alias registration tx for a 21-byte alias to a p2pkh address', async () => {
+ const chronik = new ChronikClient(
+ 'https://FakeChronikUrlToEnsureMocksOnly.com',
+ );
+
+ const mockTxid =
+ '912582a1dc11b568f14f8ebae15cbb0ce53bdb973e137e7dc7c9b261327e6cab';
+
+ chronik.broadcastTx = jest.fn().mockResolvedValue({ txid: mockTxid });
+
+ const expectedResult = {
+ explorerLink: `${currency.blockExplorerUrl}/tx/${mockTxid}`,
+ txid: mockTxid,
+ rawTxHex:
+ '0200000001d0f7ea877afcbe78a7756e9bd4cde320ed53e752b2efa1dbc92958bfa9c47212020000006a47304402205cf1941bd0ed8c49319189973feebd14e8d7faf6c5a3cdc6c16f676bd62c63ac022068c10726326e37f960433a92894609f7f0b65946d8104038110e2104d973c8e24121036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6ffffffff030000000000000000336a042e78656300157477656e74796f6e6562797465616c696173726567150020edc8389101aed204b9c17b7d64a00ead0e8cfc27020000000000001976a914638568e36d0b5d7d49a6e99854caa27d9772b09388aca48e9800000000001976a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac00000000',
+ };
+ expect(
+ await registerNewAlias(
+ chronik,
+ aliasRegisteringWalletAfterTx,
+ currency.defaultFee,
+ 'twentyonebytealiasreg',
+ 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ 551,
+ ),
+ ).toStrictEqual(expectedResult);
+ });
+ it('Broadcasts a v0 alias registration tx for a 16-byte alias to a p2pkh address', async () => {
+ const chronik = new ChronikClient(
+ 'https://FakeChronikUrlToEnsureMocksOnly.com',
+ );
+
+ const mockTxid =
+ '8783d7064ce22e8390c9fa94ef9a4d5bb0184e401ef5a9fbf60b68294e275c80';
+
+ chronik.broadcastTx = jest.fn().mockResolvedValue({ txid: mockTxid });
+
+ const expectedResult = {
+ explorerLink: `${currency.blockExplorerUrl}/tx/${mockTxid}`,
+ txid: mockTxid,
+ rawTxHex:
+ '0200000001ab6c7e3261b2c9c77d7e133e97db3be50cbb5ce1ba8e4ff168b511dca1822591020000006b483045022100d0bd27e798ac38de8b4c654c6670386c68d8bfac4bf5fe26a185d8250bd7ae7e02206acfe247b95ee9879080e6e413d7f28734aa498046e7363a638a537fc657c50e4121036ea648569566fa0843b914f67e54ebcfa6921208acd6408d2881488809403ac6ffffffff0300000000000000002e6a042e78656300107768796e6f7474687265657465737473150020edc8389101aed204b9c17b7d64a00ead0e8cfc27020000000000001976a914638568e36d0b5d7d49a6e99854caa27d9772b09388acb68a9800000000001976a91420edc8389101aed204b9c17b7d64a00ead0e8cfc88ac00000000',
+ };
+ expect(
+ await registerNewAlias(
+ chronik,
+ aliasRegisteringWalletAfterTwoTxs,
+ currency.defaultFee,
+ 'whynotthreetests',
+ 'ecash:qqswmjpcjyq6a5syh8qhklty5q826r5vlsh7a7uqtq',
+ 551,
+ ),
+ ).toStrictEqual(expectedResult);
+ });
+
it(`Throws error if called trying to send one base unit ${currency.ticker} more than available in utxo set`, async () => {
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(
chronik,
wallet,
utxos,
currency.defaultFee,
'',
false,
null,
destinationAddress,
oneBaseUnitMoreThanBalance,
);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('Insufficient funds');
const nullValuesSendBch = sendXec(
chronik,
wallet,
utxos,
currency.defaultFee,
'',
false,
null,
destinationAddress,
null,
);
await expect(nullValuesSendBch).rejects.toThrow(
new Error('Invalid singleSendValue'),
);
});
it('Throws error on attempt to send one satoshi less than backend dust limit', async () => {
const chronik = new ChronikClient(
'https://FakeChronikUrlToEnsureMocksOnly.com',
);
const { utxos, wallet, destinationAddress } = sendBCHMock;
const failedSendBch = sendXec(
chronik,
wallet,
utxos,
currency.defaultFee,
'',
false,
null,
destinationAddress,
new BigNumber(fromSatoshisToXec(currency.dustSats).toString())
.minus(new BigNumber('0.00000001'))
.toString(),
);
await 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 wallet = burnTokenWallet;
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(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 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(
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(
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(
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(
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 { expectedTxId, wallet, configObj } = createTokenMock;
const chronik = new ChronikClient(
'https://FakeChronikUrlToEnsureMocksOnly.com',
);
chronik.broadcastTx = jest
.fn()
.mockResolvedValue({ txid: expectedTxId });
expect(await createToken(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 { invalidWallet, configObj } = createTokenMock;
const chronik = new ChronikClient(
'https://FakeChronikUrlToEnsureMocksOnly.com',
);
const invalidWalletTokenCreation = createToken(
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 chronik = new ChronikClient(
'https://FakeChronikUrlToEnsureMocksOnly.com',
);
const expectedPubKey =
'03208c4f52229e021ddec5fc6e07a59fd66388ac52bc2a2c1e0f1afb24b0e275ac';
const destinationAddress =
'bitcoincash:qqvuj09f80sw9j7qru84ptxf0hyqffc38gstxfs5ru';
expect(
await getRecipientPublicKey(
chronik,
destinationAddress,
expectedPubKey,
),
).toBe(expectedPubKey);
});
});
diff --git a/cashtab/src/utils/transactions.js b/cashtab/src/utils/transactions.js
index 7016bde1b..b6c2a8959 100644
--- a/cashtab/src/utils/transactions.js
+++ b/cashtab/src/utils/transactions.js
@@ -1,616 +1,607 @@
import { currency } from 'components/Common/Ticker';
import {
fromXecToSatoshis,
isValidStoredWallet,
parseXecSendValue,
generateOpReturnScript,
generateTxInput,
generateTxOutput,
generateTokenTxInput,
generateTokenTxOutput,
signAndBuildTx,
getChangeAddressFromInputUtxos,
toHash160,
getMessageByteSize,
+ generateAliasOpReturnScript,
+ fromSatoshisToXec,
} from 'utils/cashMethods';
import ecies from 'ecies-lite';
import * as utxolib from '@bitgo/utxo-lib';
const SEND_XEC_ERRORS = {
INSUFFICIENT_FUNDS: 0,
NETWORK_ERROR: 1,
INSUFFICIENT_PRIORITY: 66, // ~insufficient fee
DOUBLE_SPENDING: 18,
MAX_UNCONFIRMED_TXS: 64,
};
export const createToken = async (
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.nonSlpUtxos;
const CREATION_ADDR = wallet.Path1899.cashAddress;
let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork(
utxolib.networks.ecash,
);
let tokenTxInputObj = generateTokenTxInput(
'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_XEC_ERRORS.INSUFFICIENT_PRIORITY;
} else if (err.error === 'txn-mempool-conflict (code 18)') {
err.code = SEND_XEC_ERRORS.DOUBLE_SPENDING;
} else if (err.error === 'Network Error') {
err.code = SEND_XEC_ERRORS.NETWORK_ERROR;
} else if (
err.error ===
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)'
) {
err.code = SEND_XEC_ERRORS.MAX_UNCONFIRMED_TXS;
}
console.log(`error: `, err);
throw err;
}
};
export const sendToken = async (
chronik,
wallet,
{ tokenId, amount, tokenReceiverAddress },
) => {
const { slpUtxos, nonSlpUtxos } = wallet.state;
const CREATION_ADDR = wallet.Path1899.cashAddress;
// Handle error of user having no XEC
if (!nonSlpUtxos || nonSlpUtxos.length === 0) {
throw new Error(
`You need some ${currency.ticker} to send ${currency.tokenTicker}`,
);
}
// instance of transaction builder
let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork(
utxolib.networks.ecash,
);
let tokenTxInputObj = generateTokenTxInput(
'SEND',
nonSlpUtxos,
slpUtxos,
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}`;
};
export const burnToken = async (chronik, wallet, { tokenId, amount }) => {
const { slpUtxos, nonSlpUtxos } = wallet.state;
const CREATION_ADDR = wallet.Path1899.cashAddress;
// Handle error of user having no XEC
if (!nonSlpUtxos || nonSlpUtxos.length === 0) {
throw new Error(`You need some ${currency.ticker} to burn eTokens`);
}
// instance of transaction builder
let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork(
utxolib.networks.ecash,
);
let tokenTxInputObj = generateTokenTxInput(
'BURN',
nonSlpUtxos,
slpUtxos,
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}`;
};
export const getRecipientPublicKey = async (
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',
);
};
export const registerNewAlias = async (
chronik,
wallet,
- utxos,
feeInSatsPerByte,
aliasName,
- registrationFee,
+ aliasAddress,
+ registrationFeeSats,
) => {
try {
+ // Instantiate new txBuilder
let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork(
utxolib.networks.ecash,
);
- const satoshisToSend = fromXecToSatoshis(registrationFee);
+ // Create this transaction with nonSlpUtxos
+ const utxos = wallet.state.nonSlpUtxos;
- // Throw validation error if fromXecToSatoshis returns false
- if (!satoshisToSend) {
- throw new Error(`Invalid alias registration fee`);
- }
+ // Build opReturnData for alias tx per spec
+ const opReturnData = generateAliasOpReturnScript(
+ aliasName,
+ aliasAddress,
+ );
- // Start of building the OP_RETURN output.
- // only build the OP_RETURN output if the user supplied it
- if (
- aliasName &&
- typeof aliasName !== 'undefined' &&
- aliasName.trim() !== ''
- ) {
- const opReturnData = generateOpReturnScript(
- aliasName,
- false, // encryption use
- false, // airdrop use
- null, // airdrop use
- null, // encrypted use
- true, // alias registration flag
- );
- txBuilder.addOutput(opReturnData, 0);
- }
+ // Add opReturn as output with 0 satoshis of XEC spent
+ txBuilder.addOutput(opReturnData, 0);
// generate the tx inputs and add to txBuilder instance
// returns the updated txBuilder, txFee, totalInputUtxoValue and inputUtxos
+ // Note that txBuilder is intentionally modified by this call
let txInputObj = generateTxInput(
- false, // not one to many
+ false, // not a one to many tx
utxos,
txBuilder,
- null, // one to many array
- satoshisToSend,
+ null, // no one-to-many array
+ registrationFeeSats,
feeInSatsPerByte,
);
+ // Get the change address
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(
- false, // not one to many
- registrationFee,
- satoshisToSend,
+ txBuilder = generateTxOutput(
+ false, // not a one-to-many tx
+ fromSatoshisToXec(registrationFeeSats), // TODO fix this oversight param req in generateTxOutput
+ registrationFeeSats,
txInputObj.totalInputUtxoValue,
currency.aliasSettings.aliasPaymentAddress,
- null, // one to many address array
+ null, // no one-to-many address array
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;
}
+ const explorerLink = `${currency.blockExplorerUrl}/tx/${broadcastResponse.txid}`;
// return the explorer link for the broadcasted tx
- return `${currency.blockExplorerUrl}/tx/${broadcastResponse.txid}`;
+ return { explorerLink, txid: broadcastResponse.txid, rawTxHex };
} catch (err) {
if (err.error === 'insufficient priority (code 66)') {
err.code = SEND_XEC_ERRORS.INSUFFICIENT_PRIORITY;
} else if (err.error === 'txn-mempool-conflict (code 18)') {
err.code = SEND_XEC_ERRORS.DOUBLE_SPENDING;
} else if (err.error === 'Network Error') {
err.code = SEND_XEC_ERRORS.NETWORK_ERROR;
} else if (
err.error ===
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)'
) {
err.code = SEND_XEC_ERRORS.MAX_UNCONFIRMED_TXS;
}
console.log(`error: `, err);
throw err;
}
};
export const sendXec = async (
chronik,
wallet,
utxos,
feeInSatsPerByte,
optionalOpReturnMsg,
isOneToMany,
destinationAddressAndValueArray,
destinationAddress,
sendAmount,
encryptionFlag,
airdropFlag,
airdropTokenId,
optionalMockPubKeyResponse = false,
) => {
try {
let txBuilder = utxolib.bitgo.createTransactionBuilderForNetwork(
utxolib.networks.ecash,
);
// 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(
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);
}
let opReturnByteCount;
if (optionalOpReturnMsg) {
opReturnByteCount = getMessageByteSize(
optionalOpReturnMsg,
encryptionFlag,
encryptedEj,
);
}
// 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,
opReturnByteCount,
);
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_XEC_ERRORS.INSUFFICIENT_PRIORITY;
} else if (err.error === 'txn-mempool-conflict (code 18)') {
err.code = SEND_XEC_ERRORS.DOUBLE_SPENDING;
} else if (err.error === 'Network Error') {
err.code = SEND_XEC_ERRORS.NETWORK_ERROR;
} else if (
err.error ===
'too-long-mempool-chain, too many unconfirmed ancestors [limit: 25] (code 64)'
) {
err.code = SEND_XEC_ERRORS.MAX_UNCONFIRMED_TXS;
}
console.log(`error: `, err);
throw err;
}
};