diff --git a/web/cashtab/src/components/Alias/Alias.js b/web/cashtab/src/components/Alias/Alias.js
index 2fd61cae1..38be39700 100644
--- a/web/cashtab/src/components/Alias/Alias.js
+++ b/web/cashtab/src/components/Alias/Alias.js
@@ -1,340 +1,340 @@
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 { AntdFormWrapper } from 'components/Common/EnhancedInputs';
import { Form, Input } 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,
} from 'utils/cashMethods';
import { isAliasAvailable, isAddressRegistered } from 'utils/chronik';
import { currency } from 'components/Common/Ticker.js';
import { registerNewAlias } from 'utils/transactions';
import {
sendXecNotification,
errorNotification,
} from 'components/Common/Notifications';
import { isAliasFormat } from 'utils/validation';
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,
synchronizeAliasCache,
} = ContextValue;
const walletState = getWalletState(wallet);
const { balances, nonSlpUtxos } = walletState;
const [formData, setFormData] = useState({
aliasName: '',
});
const [isValidAliasInput, setIsValidAliasInput] = useState(false); // tracks whether to activate the registration button
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
useEffect(() => {
passLoadingStatus(false);
}, [balances.totalBalance]);
useEffect(async () => {
// only run this useEffect block if wallet is defined
if (!wallet || typeof wallet === 'undefined') {
return;
}
passLoadingStatus(true);
// check if alias cache is sync'ed with onchain tx count, if not, update
let cachedAliases;
try {
cachedAliases = await synchronizeAliasCache(chronik);
} catch (err) {
console.log(`Error synchronizing alias cache in Alias.js`, err);
}
// check whether the address is attached to an onchain alias on page load
const walletHasAlias = isAddressRegistered(wallet, cachedAliases);
// temporary console log for reviewer
console.log(
'Does this active wallet have an onchain alias? : ' +
walletHasAlias,
);
// retrieve aliases for this active wallet from cache for rendering on the frontend
if (
walletHasAlias &&
cachedAliases &&
cachedAliases.aliases.length > 0
) {
const thisAddress = convertToEcashPrefix(
wallet.Path1899.cashAddress,
);
// filter for aliases that matches this wallet's address
const registeredAliasesToWallet = cachedAliases.aliases.filter(
alias => alias.address === thisAddress,
);
setActiveWalletAliases(registeredAliasesToWallet);
}
passLoadingStatus(false);
}, [wallet.name]);
const registerAlias = async () => {
passLoadingStatus(true);
// note: input already validated via handleAliasNameInput()
const aliasInput = formData.aliasName;
// 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(chronik, aliasInput);
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(
chronik,
wallet,
nonSlpUtxos,
currency.defaultFee,
aliasInput,
fromSatoshisToXec(registrationFee),
);
sendXecNotification(link);
} catch (err) {
handleAliasRegistrationError(err);
}
setIsValidAliasInput(true);
-
- // set alias as pending until subsequent websocket notification on 1 conf on the registration tx
-
- let tempactiveWalletAliases = activeWalletAliases;
- const thisAddress = convertToEcashPrefix(
- wallet.Path1899.cashAddress,
- );
- tempactiveWalletAliases.push({
- alias: `${aliasInput} (Pending)`,
- address: thisAddress,
- });
- setActiveWalletAliases(tempactiveWalletAliases);
} 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 handleAliasNameInput = e => {
const { name, value } = e.target;
if (value && value.trim() !== '') {
setIsValidAliasInput(true);
+ const registratioFee = getAliasRegistrationFee(value);
+ setAliasFee(registratioFee);
+ setAliasLength(new Blob([value]).size);
} else {
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 (
<>
{!balances.totalBalance ? (
You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : (
<>
{fiatPrice !== null && (
)}
>
)}
eCash Namespace Alias
handleAliasNameInput(e)
}
/>
+ {aliasLength &&
+ aliasFee &&
+ `Registration fee for this ${aliasLength} byte Alias is ${fromSatoshisToXec(
+ aliasFee,
+ )} XEC`}
registerAlias()}
>
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/web/cashtab/src/utils/__tests__/cashMethods.test.js b/web/cashtab/src/utils/__tests__/cashMethods.test.js
index 03304df1e..a17ced891 100644
--- a/web/cashtab/src/utils/__tests__/cashMethods.test.js
+++ b/web/cashtab/src/utils/__tests__/cashMethods.test.js
@@ -1,1637 +1,1798 @@
import BigNumber from 'bignumber.js';
import {
fromSatoshisToXec,
flattenContactList,
loadStoredWallet,
isValidStoredWallet,
fromLegacyDecimals,
convertToEcashPrefix,
isLegacyMigrationRequired,
convertEtokenToEcashAddr,
parseOpReturn,
convertEcashtoEtokenAddr,
getHashArrayFromWallet,
isActiveWebsocket,
parseXecSendValue,
getChangeAddressFromInputUtxos,
generateOpReturnScript,
generateTxInput,
generateTxOutput,
generateTokenTxInput,
signAndBuildTx,
fromXecToSatoshis,
getWalletBalanceFromUtxos,
signUtxosByAddress,
generateTokenTxOutput,
getCashtabByteCount,
calcFee,
toHash160,
generateGenesisOpReturn,
generateSendOpReturn,
generateBurnOpReturn,
getECPairFromWIF,
hash160ToAddress,
getAliasRegistrationFee,
outputScriptToAddress,
} from 'utils/cashMethods';
import { currency } from 'components/Common/Ticker';
import { validAddressArrayInput } from '../__mocks__/mockAddressArray';
import {
mockGenesisOpReturnScript,
mockSendOpReturnScript,
mockSendOpReturnTokenUtxos,
mockBurnOpReturnScript,
mockBurnOpReturnTokenUtxos,
} from '../__mocks__/mockOpReturnScript';
import {
cachedUtxos,
utxosLoadedFromCache,
} from '../__mocks__/mockCachedUtxos';
import {
validStoredWallet,
invalidStoredWallet,
invalidpreChronikStoredWallet,
validStoredWalletAfter20221123Streamline,
} from '../__mocks__/mockStoredWallets';
import {
missingPath1899Wallet,
missingPublicKeyInPath1899Wallet,
missingPublicKeyInPath145Wallet,
missingPublicKeyInPath245Wallet,
notLegacyWallet,
missingHash160,
} from '../__mocks__/mockLegacyWalletsUtils';
import {
shortCashtabMessageInputHex,
longCashtabMessageInputHex,
shortExternalMessageInputHex,
longExternalMessageInputHex,
shortSegmentedExternalMessageInputHex,
longSegmentedExternalMessageInputHex,
mixedSegmentedExternalMessageInputHex,
mockParsedShortCashtabMessageArray,
mockParsedLongCashtabMessageArray,
mockParsedShortExternalMessageArray,
mockParsedLongExternalMessageArray,
mockParsedShortSegmentedExternalMessageArray,
mockParsedLongSegmentedExternalMessageArray,
mockParsedMixedSegmentedExternalMessageArray,
eTokenInputHex,
mockParsedETokenOutputArray,
mockAirdropHexOutput,
mockParsedAirdropMessageArray,
} from '../__mocks__/mockOpReturnParsedArray';
import mockLegacyWallets from 'hooks/__mocks__/mockLegacyWallets';
import sendBCHMock from '../__mocks__/sendBCH';
import {
activeWebsocketAlpha,
disconnectedWebsocketAlpha,
unsubscribedWebsocket,
} from '../__mocks__/chronikWs';
import mockNonSlpUtxos from '../../hooks/__mocks__/mockNonSlpUtxos';
import mockSlpUtxos from '../../hooks/__mocks__/mockSlpUtxos';
import {
mockOneToOneSendXecTxBuilderObj,
mockOneToManySendXecTxBuilderObj,
mockCreateTokenOutputsTxBuilderObj,
mockSendTokenOutputsTxBuilderObj,
mockBurnTokenOutputsTxBuilderObj,
mockCreateTokenTxBuilderObj,
mockSendTokenTxBuilderObj,
mockBurnTokenTxBuilderObj,
} from '../__mocks__/mockTxBuilderObj';
import {
mockSingleInputUtxo,
mockMultipleInputUtxos,
mockSingleOutput,
mockMultipleOutputs,
} from '../__mocks__/mockTxBuilderData';
import createTokenMock from '../__mocks__/createToken';
import TransactionBuilder from 'utils/txBuilder';
import { mockWif, mockStringifiedECPair } from '../__mocks__/mockECPair';
+it(`Alias byte length matches for an alias input with a single emoji`, () => {
+ const aliasInput = '🙈';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
+it(`Alias byte length matches for an alias input with characters and emojis`, () => {
+ const aliasInput = 'monkey🙈';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
+it(`Alias byte length matches for an alias input with special characters`, () => {
+ const aliasInput = 'monkey©®ʕ•́ᴥ•̀ʔっ♡';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
+it(`Alias byte length matches for an alias input with Korean characters`, () => {
+ const aliasInput = '소주';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
+it(`Alias byte length matches for an alias input with Arabic characters`, () => {
+ const aliasInput = 'محيط';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
+it(`Alias byte length matches for an alias input with Chinese characters`, () => {
+ const aliasInput = '冰淇淋';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
+it(`Alias byte length matches for an alias input with a mixture of symbols, multilingual characters and emojis`, () => {
+ const aliasInput = '🙈©冰소주';
+ const aliasInputByteLength = new Blob([aliasInput]).size;
+
+ // generate the OP_RETURN script
+ const opReturnData = generateOpReturnScript(
+ aliasInput,
+ false, // encryption use
+ false, // airdrop use
+ null, // airdrop use
+ null, // encrypted use
+ true, // alias registration flag
+ );
+
+ // extract the alias input from the OP_RETURN script and check the backend size
+ const hexString = opReturnData.toString('hex'); // convert to hex
+ const opReturnAlias = parseOpReturn(hexString); // parse OP_RETURN script into array
+ const opReturnAliasStr = Buffer.from(opReturnAlias[1], 'hex'); // retrieve the alias string from array
+ const opReturnAliasByteLength = new Blob([opReturnAliasStr]).size; // get the alias string size in bytes
+
+ expect(aliasInputByteLength).toStrictEqual(opReturnAliasByteLength);
+});
+
it(`getAliasRegistrationFee() returns correct fee in sats for an alias input with 5 chars`, () => {
const aliasInput = 'panda'; // 5 chars
const regFeeResult = getAliasRegistrationFee(aliasInput);
expect(regFeeResult).toStrictEqual(
currency.aliasSettings.aliasRegistrationFeeInSats.fiveChar,
);
});
it(`getAliasRegistrationFee() returns correct fee in sats for an alias input above 8 chars`, () => {
const aliasInput = 'pandapanda'; // 10 chars
const regFeeResult = getAliasRegistrationFee(aliasInput);
expect(regFeeResult).toStrictEqual(
currency.aliasSettings.aliasRegistrationFeeInSats.eightChar,
);
});
it(`generateSendOpReturn() returns correct script object for valid tokenUtxo and send quantity`, () => {
const tokensToSend = 50;
const sendOpReturnScriptObj = generateSendOpReturn(
mockSendOpReturnTokenUtxos,
tokensToSend,
);
expect(JSON.stringify(sendOpReturnScriptObj.script)).toStrictEqual(
JSON.stringify(mockSendOpReturnScript),
);
});
it(`generateSendOpReturnScript() throws error on invalid input`, () => {
const mockSendOpReturnTokenUtxos = null;
const tokensToSend = 50;
let errorThrown;
try {
generateSendOpReturn(mockSendOpReturnTokenUtxos, tokensToSend);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid send token parameter');
});
it(`generateBurnOpReturn() returns correct script for valid tokenUtxo and burn quantity`, () => {
const tokensToBurn = 7000;
const burnOpReturnScript = generateBurnOpReturn(
mockBurnOpReturnTokenUtxos,
tokensToBurn,
);
expect(JSON.stringify(burnOpReturnScript)).toStrictEqual(
JSON.stringify(mockBurnOpReturnScript),
);
});
it(`generateBurnOpReturn() throws error on invalid input`, () => {
const tokensToBurn = 7000;
let errorThrown;
try {
generateBurnOpReturn(null, tokensToBurn);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid burn token parameter');
});
it(`generateGenesisOpReturn() returns correct script for a valid configObj`, () => {
const configObj = {
name: 'ethantest',
ticker: 'ETN',
documentUrl: 'https://cashtab.com/',
decimals: '3',
initialQty: '5000',
documentHash: '',
mintBatonVout: null,
};
const genesisOpReturnScript = generateGenesisOpReturn(configObj);
expect(JSON.stringify(genesisOpReturnScript)).toStrictEqual(
JSON.stringify(mockGenesisOpReturnScript),
);
});
it(`generateGenesisOpReturn() throws error on invalid configObj`, () => {
const configObj = null;
let errorThrown;
try {
generateGenesisOpReturn(configObj);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid token configuration');
});
it(`signUtxosByAddress() successfully returns a txBuilder object for a one to one XEC tx`, () => {
const isOneToMany = false;
const { destinationAddress, wallet, utxos } = sendBCHMock;
let txBuilder = new TransactionBuilder();
const satoshisToSendInput = new BigNumber(2184);
const feeInSatsPerByte = currency.defaultFee;
// mock tx input
const inputObj = generateTxInput(
isOneToMany,
utxos,
txBuilder,
null,
satoshisToSendInput,
feeInSatsPerByte,
);
// mock tx output
const totalInputUtxoValue =
mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value;
const singleSendValue = new BigNumber(
fromSatoshisToXec(
mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value,
),
);
const satoshisToSendOutput = fromXecToSatoshis(
new BigNumber(singleSendValue),
);
const txFee = new BigNumber(totalInputUtxoValue).minus(
new BigNumber(satoshisToSendOutput),
);
const changeAddress = wallet.Path1899.cashAddress;
const outputObj = generateTxOutput(
isOneToMany,
singleSendValue,
satoshisToSendOutput,
totalInputUtxoValue,
destinationAddress,
null,
changeAddress,
txFee,
inputObj.txBuilder,
);
const txBuilderResponse = signUtxosByAddress(
mockSingleInputUtxo,
wallet,
outputObj,
);
expect(txBuilderResponse.toString()).toStrictEqual(
mockOneToOneSendXecTxBuilderObj.toString(),
);
});
it(`signUtxosByAddress() successfully returns a txBuilder object for a one to many XEC tx`, () => {
const isOneToMany = true;
const { wallet, utxos } = sendBCHMock;
let txBuilder = new TransactionBuilder();
let destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000',
];
const satoshisToSendInput = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
// mock tx input
const inputObj = generateTxInput(
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSendInput,
feeInSatsPerByte,
);
// mock tx output
const totalInputUtxoValue =
mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value;
const singleSendValue = null;
const satoshisToSendOutput = new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value,
);
const txFee = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSendOutput)
.minus(
new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value,
),
); // change value
destinationAddressAndValueArray = validAddressArrayInput;
const changeAddress = wallet.Path1899.cashAddress;
const outputObj = generateTxOutput(
isOneToMany,
singleSendValue,
satoshisToSendOutput,
totalInputUtxoValue,
null,
destinationAddressAndValueArray,
changeAddress,
txFee,
inputObj.txBuilder,
);
const txBuilderResponse = signUtxosByAddress(
mockSingleInputUtxo,
wallet,
outputObj,
);
expect(txBuilderResponse.toString()).toStrictEqual(
mockOneToManySendXecTxBuilderObj.toString(),
);
});
it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo`, () => {
const { wallet } = sendBCHMock;
const inputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
address: 'bitcoincash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl',
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
const changeAddress = getChangeAddressFromInputUtxos(inputUtxo, wallet);
expect(changeAddress).toStrictEqual(inputUtxo[0].address);
});
it(`getChangeAddressFromInputUtxos() returns a correct change address from a valid inputUtxo and accepts ecash: format`, () => {
const { wallet } = sendBCHMock;
const inputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
address: 'ecash:qphpmfj0qn7znklqhrfn5dq7qh36l3vxav9up3h67g',
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
const changeAddress = getChangeAddressFromInputUtxos(inputUtxo, wallet);
expect(changeAddress).toStrictEqual(inputUtxo[0].address);
});
it(`getChangeAddressFromInputUtxos() throws error upon a malformed input utxo`, () => {
const { wallet } = sendBCHMock;
const invalidInputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
let thrownError;
try {
getChangeAddressFromInputUtxos(invalidInputUtxo, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid input utxo');
});
it(`getChangeAddressFromInputUtxos() throws error upon a valid input utxo with invalid address param`, () => {
const { wallet } = sendBCHMock;
const invalidInputUtxo = [
{
height: 669639,
tx_hash:
'0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
tx_pos: 0,
value: 1000,
address: 'bitcoincash:1qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', // invalid cash address
txid: '0da6d49cf95d4603958e53360ad1e90bfccef41bfb327d6b2e8a77e242fa2d58',
vout: 0,
isValid: false,
wif: 'L58jqHoi5ynSdsskPVBJuGuVqTP8ZML1MwHQsBJY32Pv7cqDSCeH',
},
];
let thrownError;
try {
getChangeAddressFromInputUtxos(invalidInputUtxo, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid input utxo');
});
it(`getChangeAddressFromInputUtxos() throws an error upon a null inputUtxos param`, () => {
const { wallet } = sendBCHMock;
const inputUtxo = null;
let thrownError;
try {
getChangeAddressFromInputUtxos(inputUtxo, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual(
'Invalid getChangeAddressFromWallet input parameter',
);
});
it(`parseXecSendValue() correctly parses the value for a valid one to one send XEC transaction`, () => {
expect(parseXecSendValue(false, '550', null)).toStrictEqual(
new BigNumber(550),
);
});
it(`parseXecSendValue() correctly parses the value for a valid one to many send XEC transaction`, () => {
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,6',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,6',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,6',
];
expect(
parseXecSendValue(true, null, destinationAddressAndValueArray),
).toStrictEqual(new BigNumber(18));
});
it(`parseXecSendValue() correctly throws error when singleSendValue is invalid for a one to one send XEC transaction`, () => {
let errorThrown;
try {
parseXecSendValue(false, null, 550);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('Invalid singleSendValue');
});
it(`parseXecSendValue() correctly throws error when destinationAddressAndValueArray is invalid for a one to many send XEC transaction`, () => {
let errorThrown;
try {
parseXecSendValue(true, null, null);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual(
'Invalid destinationAddressAndValueArray',
);
});
it(`parseXecSendValue() correctly throws error when the total value for a one to one send XEC transaction is below dust`, () => {
let errorThrown;
try {
parseXecSendValue(false, '4.5', null);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('dust');
});
it(`parseXecSendValue() correctly throws error when the total value for a one to many send XEC transaction is below dust`, () => {
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,2',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,2',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,1',
];
let errorThrown;
try {
parseXecSendValue(true, null, destinationAddressAndValueArray);
} catch (err) {
errorThrown = err;
}
expect(errorThrown.message).toStrictEqual('dust');
});
it('generateOpReturnScript() correctly generates an encrypted message script', () => {
const optionalOpReturnMsg = 'testing generateOpReturnScript()';
const encryptionFlag = true;
const airdropFlag = false;
const airdropTokenId = null;
const mockEncryptedEj =
'04688f9907fe3c7c0b78a73c4ab4f75e15e7e2b79641add519617086126fe6f6b1405a14eed48e90c9c8c0fc77f0f36984a78173e76ce51f0a44af94b59e9da703c9ff82758cfdb9cc46437d662423400fb731d3bfc1df0599279356ca261213fbb40d398c041e1bac966afed1b404581ab1bcfcde1fa039d53b7c7b70e8edf26d64bea9fbeed24cc80909796e6af5863707fa021f2a2ebaa2fe894904702be19d';
const encodedScript = generateOpReturnScript(
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
mockEncryptedEj,
);
expect(encodedScript.toString('hex')).toBe(
'6a04657461624d420130343638386639393037666533633763306237386137336334616234663735653135653765326237393634316164643531393631373038363132366665366636623134303561313465656434386539306339633863306663373766306633363938346137383137336537366365353166306134346166393462353965396461373033633966663832373538636664623963633436343337643636323432333430306662373331643362666331646630353939323739333536636132363132313366626234306433393863303431653162616339363661666564316234303435383161623162636663646531666130333964353362376337623730653865646632366436346265613966626565643234636338303930393739366536616635383633373037666130323166326132656261613266653839343930343730326265313964',
);
});
it('generateOpReturnScript() correctly generates an un-encrypted non-airdrop message script', () => {
const optionalOpReturnMsg = 'testing generateOpReturnScript()';
const encryptionFlag = false;
const airdropFlag = false;
const encodedScript = generateOpReturnScript(
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
);
expect(encodedScript.toString('hex')).toBe(
'6a04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829',
);
});
it('generateOpReturnScript() correctly generates an un-encrypted airdrop message script', () => {
const optionalOpReturnMsg = 'testing generateOpReturnScript()';
const encryptionFlag = false;
const airdropFlag = true;
const airdropTokenId =
'1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
const encodedScript = generateOpReturnScript(
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
);
expect(encodedScript.toString('hex')).toBe(
'6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e04007461622074657374696e672067656e65726174654f7052657475726e5363726970742829',
);
});
it('generateOpReturnScript() correctly generates an un-encrypted airdrop with no message script', () => {
const optionalOpReturnMsg = null;
const encryptionFlag = false;
const airdropFlag = true;
const airdropTokenId =
'1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
const encodedScript = generateOpReturnScript(
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
);
expect(encodedScript.toString('hex')).toBe(
'6a0464726f70201c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e0400746162',
);
});
it('generateOpReturnScript() correctly throws an error on an invalid encryption input', () => {
const optionalOpReturnMsg = null;
const encryptionFlag = true;
const airdropFlag = false;
const airdropTokenId = null;
const mockEncryptedEj = null; // invalid given encryptionFlag is true
let thrownError;
try {
generateOpReturnScript(
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
mockEncryptedEj,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input');
});
it('generateOpReturnScript() correctly throws an error on an invalid airdrop input', () => {
const optionalOpReturnMsg = null;
const encryptionFlag = false;
const airdropFlag = true;
const airdropTokenId = null; // invalid given airdropFlag is true
let thrownError;
try {
generateOpReturnScript(
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid OP RETURN script input');
});
it('generateOpReturnScript() correctly generates an alias registration script', () => {
const optionalOpReturnMsg = 'nfs'; // the alias name to be registered
const encodedScript = generateOpReturnScript(
optionalOpReturnMsg,
false,
false,
null,
null,
true, // alias registration flag
);
expect(encodedScript.toString('hex')).toBe('6a042e786563036e6673');
});
it(`generateTokenTxInput() returns a valid object for a valid create token tx`, async () => {
let txBuilder = new TransactionBuilder();
const tokenId =
'1c6c9c64d70b285befe733f175d0f384538576876bd280b10587df81279d3f5e';
const tokenInputObj = generateTokenTxInput(
'GENESIS',
mockNonSlpUtxos,
null, // no slpUtxos used for genesis tx
tokenId,
null, // no token send/burn amount for genesis tx
currency.defaultFee,
txBuilder,
);
expect(tokenInputObj.inputXecUtxos).toStrictEqual(
[mockNonSlpUtxos[0]].concat([mockNonSlpUtxos[1]]),
);
expect(tokenInputObj.txBuilder.toString()).toStrictEqual(
mockCreateTokenTxBuilderObj.toString(),
);
expect(tokenInputObj.remainderXecValue).toStrictEqual(
new BigNumber(699702), // tokenInputObj.inputXecUtxos - currency.etokenSats 546 - txFee
);
});
it(`generateTokenTxInput() returns a valid object for a valid send token tx`, async () => {
let txBuilder = new TransactionBuilder();
const tokenId = mockSlpUtxos[0].tokenId;
const tokenInputObj = generateTokenTxInput(
'SEND',
mockNonSlpUtxos,
mockSlpUtxos,
tokenId,
new BigNumber(500), // sending 500 of these tokens
currency.defaultFee,
txBuilder,
);
expect(tokenInputObj.inputTokenUtxos).toStrictEqual(
[mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500
);
expect(tokenInputObj.remainderTokenValue).toStrictEqual(
new BigNumber(6400), // token change is mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 - [tokenAmount] 500
);
expect(tokenInputObj.txBuilder.toString()).toStrictEqual(
mockSendTokenTxBuilderObj.toString(),
);
});
it(`generateTokenTxInput() returns a valid object for a valid burn token tx`, async () => {
let txBuilder = new TransactionBuilder();
const tokenId = mockSlpUtxos[0].tokenId;
const tokenInputObj = generateTokenTxInput(
'BURN',
mockNonSlpUtxos,
mockSlpUtxos,
tokenId,
new BigNumber(500), // burning 500 of these tokens
currency.defaultFee,
txBuilder,
);
expect(tokenInputObj.inputTokenUtxos).toStrictEqual(
[mockSlpUtxos[0]].concat([mockSlpUtxos[1]]), // mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500
);
expect(tokenInputObj.remainderTokenValue).toStrictEqual(
new BigNumber(6400), // token change is mockSlpUtxos[0] 400 + mockSlpUtxos[1] 6500 - [tokenAmount] 500
);
expect(tokenInputObj.txBuilder.toString()).toStrictEqual(
mockBurnTokenTxBuilderObj.toString(),
);
});
it(`generateTokenTxOutput() returns a valid object for a valid create token tx`, async () => {
let txBuilder = new TransactionBuilder();
const { configObj, wallet } = createTokenMock;
const tokenSenderCashAddress = wallet.Path1899.cashAddress;
const tokenOutputObj = generateTokenTxOutput(
txBuilder,
'GENESIS',
tokenSenderCashAddress,
null, // optional, for SEND or BURN amount
new BigNumber(500), // remainder XEC value
configObj,
);
expect(tokenOutputObj.toString()).toStrictEqual(
mockCreateTokenOutputsTxBuilderObj.toString(),
);
});
it(`generateTokenTxOutput() returns a valid object for a valid send token tx`, async () => {
let txBuilder = new TransactionBuilder();
const { wallet } = createTokenMock;
const tokenSenderCashAddress = wallet.Path1899.cashAddress;
const tokenRecipientTokenAddress = wallet.Path1899.cashAddress;
const tokenOutputObj = generateTokenTxOutput(
txBuilder,
'SEND',
tokenSenderCashAddress,
mockSlpUtxos,
new BigNumber(500), // remainder XEC value
null, // only for genesis tx
tokenRecipientTokenAddress, // recipient token address
new BigNumber(50),
);
expect(tokenOutputObj.toString()).toStrictEqual(
mockSendTokenOutputsTxBuilderObj.toString(),
);
});
it(`generateTokenTxOutput() returns a valid object for a valid burn token tx`, async () => {
let txBuilder = new TransactionBuilder();
const { wallet } = createTokenMock;
const tokenSenderCashAddress = wallet.Path1899.cashAddress;
const tokenOutputObj = generateTokenTxOutput(
txBuilder,
'BURN',
tokenSenderCashAddress,
mockSlpUtxos,
new BigNumber(500), // remainder XEC value
null, // only for genesis tx
null, // no token recipients for burn tx
new BigNumber(50),
);
expect(tokenOutputObj.toString()).toStrictEqual(
mockBurnTokenOutputsTxBuilderObj.toString(),
);
});
it(`generateTxInput() returns an input object for a valid one to one XEC tx`, async () => {
const isOneToMany = false;
const utxos = mockNonSlpUtxos;
let txBuilder = new TransactionBuilder();
const destinationAddressAndValueArray = null;
const satoshisToSend = new BigNumber(2184);
const feeInSatsPerByte = currency.defaultFee;
const inputObj = generateTxInput(
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
expect(inputObj.txBuilder).not.toStrictEqual(null);
expect(inputObj.totalInputUtxoValue).toStrictEqual(new BigNumber(701000));
expect(inputObj.txFee).toStrictEqual(752);
expect(inputObj.inputUtxos.length).not.toStrictEqual(0);
});
it(`generateTxInput() returns an input object for a valid one to many XEC tx`, async () => {
const isOneToMany = true;
const utxos = mockNonSlpUtxos;
let txBuilder = new TransactionBuilder();
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000',
];
const satoshisToSend = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
const inputObj = generateTxInput(
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
expect(inputObj.txBuilder).not.toStrictEqual(null);
expect(inputObj.totalInputUtxoValue).toStrictEqual(new BigNumber(1401000));
expect(inputObj.txFee).toStrictEqual(1186);
expect(inputObj.inputUtxos.length).not.toStrictEqual(0);
});
it(`generateTxInput() throws error for a one to many XEC tx with invalid destinationAddressAndValueArray input`, async () => {
const isOneToMany = true;
const utxos = mockNonSlpUtxos;
let txBuilder = new TransactionBuilder();
const destinationAddressAndValueArray = null; // invalid since isOneToMany is true
const satoshisToSend = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
let thrownError;
try {
generateTxInput(
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`generateTxInput() throws error for a one to many XEC tx with invalid utxos input`, async () => {
const isOneToMany = true;
const utxos = null;
let txBuilder = new TransactionBuilder();
const destinationAddressAndValueArray = [
'ecash:qrmz0egsqxj35x5jmzf8szrszdeu72fx0uxgwk3r48,3000',
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx,3000',
'ecash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,3000',
];
const satoshisToSend = new BigNumber(900000);
const feeInSatsPerByte = currency.defaultFee;
let thrownError;
try {
generateTxInput(
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`generateTxOutput() returns a txBuilder instance for a valid one to one XEC tx`, () => {
// txbuilder output params
const { destinationAddress, wallet } = sendBCHMock;
const isOneToMany = false;
const singleSendValue = fromSatoshisToXec(
mockOneToOneSendXecTxBuilderObj.transaction.tx.outs[0].value,
);
const totalInputUtxoValue =
mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value;
const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue));
// for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue
// no change output to be subtracted in this tx
const txFee = new BigNumber(totalInputUtxoValue).minus(
new BigNumber(satoshisToSend),
);
const destinationAddressAndValueArray = null;
let txBuilder = new TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
const outputObj = generateTxOutput(
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
destinationAddress,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
expect(outputObj.toString()).toStrictEqual(
mockOneToOneSendXecTxBuilderObj.toString(),
);
});
it(`generateTxOutput() returns a txBuilder instance for a valid one to many XEC tx`, () => {
// txbuilder output params
const { destinationAddress, wallet } = sendBCHMock;
const isOneToMany = true;
const singleSendValue = null;
const totalInputUtxoValue =
mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value;
const satoshisToSend = new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value,
);
// for unit test purposes, calculate fee by subtracting satoshisToSend and change amount from totalInputUtxoValue
const txFee = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSend)
.minus(
new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value,
),
); // change value
const destinationAddressAndValueArray = validAddressArrayInput;
let txBuilder = new TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
const outputObj = generateTxOutput(
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
destinationAddress,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
expect(outputObj.toString()).toStrictEqual(
mockOneToManySendXecTxBuilderObj.toString(),
);
});
it(`generateTxOutput() throws an error on invalid input params for a one to one XEC tx`, () => {
// txbuilder output params
const { wallet } = sendBCHMock;
const isOneToMany = false;
const singleSendValue = null; // invalid due to singleSendValue being mandatory when isOneToMany is false
const totalInputUtxoValue =
mockOneToOneSendXecTxBuilderObj.transaction.inputs[0].value;
const satoshisToSend = fromXecToSatoshis(new BigNumber(singleSendValue));
// for unit test purposes, calculate fee by subtracting satoshisToSend from totalInputUtxoValue
// no change output to be subtracted in this tx
const txFee = new BigNumber(totalInputUtxoValue).minus(satoshisToSend);
const destinationAddressAndValueArray = null;
let txBuilder = new TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
let thrownError;
try {
generateTxOutput(
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
null,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`generateTxOutput() throws an error on invalid input params for a one to many XEC tx`, () => {
// txbuilder output params
const { wallet } = sendBCHMock;
const isOneToMany = true;
const singleSendValue = null;
const totalInputUtxoValue =
mockOneToManySendXecTxBuilderObj.transaction.inputs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.inputs[2].value;
const satoshisToSend = new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[0].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[1].value +
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[2].value,
);
// for unit test purposes, calculate fee by subtracting satoshisToSend and change amount from totalInputUtxoValue
const txFee = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSend)
.minus(
new BigNumber(
mockOneToManySendXecTxBuilderObj.transaction.tx.outs[3].value,
),
); // change value
const destinationAddressAndValueArray = null; // invalid as this is mandatory when isOneToMany is true
let txBuilder = new TransactionBuilder();
const changeAddress = wallet.Path1899.cashAddress;
let thrownError;
try {
generateTxOutput(
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
null,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid tx input parameter');
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and a single output`, () => {
// txbuilder output params
let txBuilder = new TransactionBuilder();
// Use legacy sequencing for legacy unit tests
txBuilder.DEFAULT_SEQUENCE = 0xffffffff;
const { wallet } = sendBCHMock;
// add inputs to txBuilder
txBuilder.addInput(
mockSingleInputUtxo[0].txid,
mockSingleInputUtxo[0].vout,
);
// add outputs to txBuilder
const outputAddressAndValue = mockSingleOutput.split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value
);
const rawTxHex = signAndBuildTx(mockSingleInputUtxo, txBuilder, wallet);
expect(rawTxHex).toStrictEqual(
'0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100b4ee5268cb64c4f097e739df7c6934d1df7e75a4f217d5824db18ae2e12554b102204faf039738181aae80c064b928b3d8079a82cdb080ce9a2d5453939a588f4372412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with a single input and multiple outputs`, () => {
// txbuilder output params
let txBuilder = new TransactionBuilder();
// Use legacy sequencing for legacy unit tests
txBuilder.DEFAULT_SEQUENCE = 0xffffffff;
const { wallet } = sendBCHMock;
// add inputs to txBuilder
txBuilder.addInput(
mockSingleInputUtxo[0].txid,
mockSingleInputUtxo[0].vout,
);
// add outputs to txBuilder
for (let i = 0; i < mockMultipleOutputs.length; i++) {
const outputAddressAndValue = mockMultipleOutputs[i].split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(
fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])),
), // value
);
}
const rawTxHex = signAndBuildTx(mockSingleInputUtxo, txBuilder, wallet);
expect(rawTxHex).toStrictEqual(
'0200000001582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006b483045022100df29734c4fb348b0e8b613ce522c10c5ac14cb3ecd32843dc7fcf004d60f1b8a022023c4ae02b38c7272e29f344902ae2afa4db1ec37d582a31c16650a0abc4f480c412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and a single output`, () => {
// txbuilder output params
let txBuilder = new TransactionBuilder();
// Use legacy sequencing for legacy unit tests
txBuilder.DEFAULT_SEQUENCE = 0xffffffff;
const { wallet } = sendBCHMock;
// add inputs to txBuilder
for (let i = 0; i < mockMultipleInputUtxos.length; i++) {
txBuilder.addInput(
mockMultipleInputUtxos[i].txid,
mockMultipleInputUtxos[i].vout,
);
}
// add outputs to txBuilder
const outputAddressAndValue = mockSingleOutput.split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(fromXecToSatoshis(new BigNumber(outputAddressAndValue[1]))), // value
);
const rawTxHex = signAndBuildTx(mockMultipleInputUtxos, txBuilder, wallet);
expect(rawTxHex).toStrictEqual(
'0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a4730440220541366dd5ea25d65d3044dbde16fc6118ab1aee07c7d0d4c25c9e8aa299f040402203ed2f540948197d4c6a4ae963ad187d145a9fb339e311317b03c6172732e267b412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006b483045022100c1d02c5023f83b87a4f2dd26a7306ed9be9d53ab972bd935b440e45eb54a304302200b99aa2f1a728b3bb1dcbff80742c5fcab991bb74e80fa231255a31d58a6ff7d412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006b483045022100bd24d11d7070988848cb4aa2b10748aa0aeb79dc8af39c1f22dc1034b3121e5f02201491026e5f8f6eb566eb17cb195e3da3ff0d9cf01bdd34c944964d33a8d3b1ad412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0158020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() successfully returns a raw tx hex for a tx with multiple inputs and multiple outputs`, () => {
// txbuilder output params
let txBuilder = new TransactionBuilder();
// Use legacy sequencing for legacy unit tests
txBuilder.DEFAULT_SEQUENCE = 0xffffffff;
const { wallet } = sendBCHMock;
// add inputs to txBuilder
for (let i = 0; i < mockMultipleInputUtxos.length; i++) {
txBuilder.addInput(
mockMultipleInputUtxos[i].txid,
mockMultipleInputUtxos[i].vout,
);
}
// add outputs to txBuilder
for (let i = 0; i < mockMultipleOutputs.length; i++) {
const outputAddressAndValue = mockMultipleOutputs[i].split(',');
txBuilder.addOutput(
outputAddressAndValue[0], // address
parseInt(
fromXecToSatoshis(new BigNumber(outputAddressAndValue[1])),
), // value
);
}
const rawTxHex = signAndBuildTx(mockMultipleInputUtxos, txBuilder, wallet);
expect(rawTxHex).toStrictEqual(
'0200000003582dfa42e2778a2e6b7d32fb1bf4cefc0be9d10a36538e9503465df99cd4a60d000000006a47304402203de4e6a512a6bec1d378b6444008484e1be5a0c621dc4b201d67addefffe864602202daf82e76b7594fe1ab54a49380c6b1226ab65551ae6ab9164216b66266f34a1412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff7313e804af08113dfa290515390a8ec3ac01448118f2eb556ee168a96ee6acdd000000006a473044022029f5fcbc9356beb9eae6b9ff9a479e8c8331b95406b6be456fccf9d90f148ea1022028f4e7fa7234f9429535360c8f5dad303e2c5044431615997861b10f26fa8a88412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff960dd2f0c47e8a3cf1486b046d879f45a047da3b51aedfb5594138ac857214f1000000006a473044022049a67738d99006b3523cff818f3626104cf5106bd463be70d22ad179a8cb403b022025829baf67f964202ea77ea7462a5447e32415e7293cdee382ea7ae9374364e8412102322fe90c5255fe37ab321c386f9446a86e80c3940701d430f22325094fdcec60ffffffff0326020000000000001976a914f627e51001a51a1a92d8927808701373cf29267f88ac26020000000000001976a9140b7d35fda03544a08e65464d54cfae4257eb6db788ac26020000000000001976a9149846b6b38ff713334ac19fe3cf851a1f98c07b0088ac00000000',
);
});
it(`signAndBuildTx() throws error on an empty inputUtxo param`, () => {
// txbuilder output params
let txBuilder = new TransactionBuilder();
const { wallet } = sendBCHMock;
let thrownError;
try {
signAndBuildTx([], txBuilder, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid buildTx parameter');
});
it(`signAndBuildTx() throws error on a null inputUtxo param`, () => {
// txbuilder output params
let txBuilder = new TransactionBuilder();
const inputUtxo = null; // invalid input param
const { wallet } = sendBCHMock;
let thrownError;
try {
signAndBuildTx(inputUtxo, txBuilder, wallet);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toStrictEqual('Invalid buildTx parameter');
});
describe('Correctly executes cash utility functions', () => {
it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 2`, () => {
expect(fromSatoshisToXec(1, 2)).toStrictEqual(new BigNumber(0.01));
});
it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 2`, () => {
expect(fromSatoshisToXec(1000000012345678, 2)).toStrictEqual(
new BigNumber(10000000123456.78),
);
});
it(`Correctly converts smallest base unit to smallest decimal for cashDecimals = 8`, () => {
expect(fromSatoshisToXec(1, 8)).toStrictEqual(
new BigNumber(0.00000001),
);
});
it(`Correctly converts largest base unit to smallest decimal for cashDecimals = 8`, () => {
expect(fromSatoshisToXec(1000000012345678, 8)).toStrictEqual(
new BigNumber(10000000.12345678),
);
});
it(`Accepts a cachedWalletState that has not preserved BigNumber object types, and returns the same wallet state with BigNumber type re-instituted`, () => {
expect(loadStoredWallet(cachedUtxos)).toStrictEqual(
utxosLoadedFromCache,
);
});
it(`loadStoredWallet accepts undefined wallet state as input and outputs a zero balance wallet state`, () => {
expect(loadStoredWallet(undefined)).toStrictEqual({
balances: {
totalBalanceInSatoshis: '0',
totalBalance: '0',
},
});
});
it(`Correctly determines a wallet's balance from its set of non-eToken utxos (nonSlpUtxos)`, () => {
expect(
getWalletBalanceFromUtxos(
validStoredWalletAfter20221123Streamline.state.nonSlpUtxos,
),
).toStrictEqual(validStoredWallet.state.balances);
});
it(`Correctly determines a wallet's zero balance from its empty set of non-eToken utxos (nonSlpUtxos)`, () => {
expect(
getWalletBalanceFromUtxos(utxosLoadedFromCache.nonSlpUtxos),
).toStrictEqual(utxosLoadedFromCache.balances);
});
it(`Recognizes a stored wallet as valid if it has all required fields prior to 20221123 updated format`, () => {
expect(isValidStoredWallet(validStoredWallet)).toBe(true);
});
it(`Recognizes a stored wallet as valid if it has all required fields in 20221123 updated format`, () => {
expect(
isValidStoredWallet(validStoredWalletAfter20221123Streamline),
).toBe(true);
});
it(`Recognizes a stored wallet as invalid if it is missing required fields`, () => {
expect(isValidStoredWallet(invalidStoredWallet)).toBe(false);
});
it(`Recognizes a stored wallet as invalid if it includes hydratedUtxoDetails in the state field`, () => {
expect(isValidStoredWallet(invalidpreChronikStoredWallet)).toBe(false);
});
it(`Converts a legacy BCH amount to an XEC amount`, () => {
expect(fromLegacyDecimals(0.00000546, 2)).toStrictEqual(5.46);
});
it(`Leaves a legacy BCH amount unchanged if currency.cashDecimals is 8`, () => {
expect(fromLegacyDecimals(0.00000546, 8)).toStrictEqual(0.00000546);
});
it(`convertToEcashPrefix converts a bitcoincash: prefixed address to an ecash: prefixed address`, () => {
expect(
convertToEcashPrefix(
'bitcoincash:qz2708636snqhsxu8wnlka78h6fdp77ar5ulhz04hr',
),
).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035');
});
it(`convertToEcashPrefix returns an ecash: prefix address unchanged`, () => {
expect(
convertToEcashPrefix(
'ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035',
),
).toBe('ecash:qz2708636snqhsxu8wnlka78h6fdp77ar59jrf5035');
});
it(`Recognizes a wallet with missing Path1889 is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingPath1899Wallet)).toBe(true);
});
it(`Recognizes a wallet with missing PublicKey in Path1889 is a Legacy Wallet and requires migration`, () => {
expect(
isLegacyMigrationRequired(missingPublicKeyInPath1899Wallet),
).toBe(true);
});
it(`Recognizes a wallet with missing PublicKey in Path145 is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingPublicKeyInPath145Wallet)).toBe(
true,
);
});
it(`Recognizes a wallet with missing PublicKey in Path245 is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingPublicKeyInPath245Wallet)).toBe(
true,
);
});
it(`Recognizes a wallet with missing Hash160 values is a Legacy Wallet and requires migration`, () => {
expect(isLegacyMigrationRequired(missingHash160)).toBe(true);
});
it(`Recognizes a latest, current wallet that does not require migration`, () => {
expect(isLegacyMigrationRequired(notLegacyWallet)).toBe(false);
});
test('toHash160() converts a valid bitcoincash: prefix address to a hash160', () => {
const result = toHash160(
'bitcoincash:qq9h6d0a5q65fgywv4ry64x04ep906mdku7ymranw3',
);
expect(result).toStrictEqual(
'0b7d35fda03544a08e65464d54cfae4257eb6db7',
);
});
test('toHash160 throws error if input address is an invalid bitcoincash: address', () => {
const address = 'bitcoincash:qqd3qnINVALIDDDDDDDDDza25m';
let errorThrown;
try {
toHash160(address);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.');
});
test('toHash160() converts a valid ecash: prefix address to a hash160', () => {
const result = toHash160(
'ecash:qq9h6d0a5q65fgywv4ry64x04ep906mdku8f0gxfgx',
);
expect(result).toStrictEqual(
'0b7d35fda03544a08e65464d54cfae4257eb6db7',
);
});
test('toHash160 throws error if input address is an invalid ecash address', () => {
const address = 'ecash:qqd3qn4zINVALIDDDDDtfza25m';
let errorThrown;
try {
toHash160(address);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.');
});
test('toHash160() converts a valid etoken: address to a hash160', () => {
const result = toHash160(
'etoken:qq9h6d0a5q65fgywv4ry64x04ep906mdkufhx2swv3',
);
expect(result).toStrictEqual(
'0b7d35fda03544a08e65464d54cfae4257eb6db7',
);
});
test('toHash160 throws error if input address is an invalid etoken: address', () => {
const address = 'etoken:qq9h6d0a5INVALIDDDDDDx2swv3';
let errorThrown;
try {
toHash160(address);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.');
});
test('toHash160() converts a valid simpleledger: address to a hash160', () => {
const result = toHash160(
'simpleledger:qq9h6d0a5q65fgywv4ry64x04ep906mdkujlscgns0',
);
expect(result).toStrictEqual(
'0b7d35fda03544a08e65464d54cfae4257eb6db7',
);
});
test('toHash160 throws error if input address is an invalid simpleledger: address', () => {
const address = 'simpleledger:qq9h6d0a5qINVALIDDDjlscgns0';
let errorThrown;
try {
toHash160(address);
} catch (err) {
errorThrown = err.message;
}
expect(errorThrown).toStrictEqual('Invalid address: ' + address + '.');
});
test('parseOpReturn() successfully parses a short cashtab message', async () => {
const result = parseOpReturn(shortCashtabMessageInputHex);
expect(result).toStrictEqual(mockParsedShortCashtabMessageArray);
});
test('parseOpReturn() successfully parses a long cashtab message where an additional PUSHDATA1 is present', async () => {
const result = parseOpReturn(longCashtabMessageInputHex);
expect(result).toStrictEqual(mockParsedLongCashtabMessageArray);
});
test('parseOpReturn() successfully parses a short external message', async () => {
const result = parseOpReturn(shortExternalMessageInputHex);
expect(result).toStrictEqual(mockParsedShortExternalMessageArray);
});
test('parseOpReturn() successfully parses a long external message where an additional PUSHDATA1 is present', async () => {
const result = parseOpReturn(longExternalMessageInputHex);
expect(result).toStrictEqual(mockParsedLongExternalMessageArray);
});
test('parseOpReturn() successfully parses an external message that is segmented into separate short parts', async () => {
const result = parseOpReturn(shortSegmentedExternalMessageInputHex);
expect(result).toStrictEqual(
mockParsedShortSegmentedExternalMessageArray,
);
});
test('parseOpReturn() successfully parses an external message that is segmented into separate long parts', async () => {
const result = parseOpReturn(longSegmentedExternalMessageInputHex);
expect(result).toStrictEqual(
mockParsedLongSegmentedExternalMessageArray,
);
});
test('parseOpReturn() successfully parses an external message that is segmented into separate long and short parts', async () => {
const result = parseOpReturn(mixedSegmentedExternalMessageInputHex);
expect(result).toStrictEqual(
mockParsedMixedSegmentedExternalMessageArray,
);
});
test('parseOpReturn() successfully parses an eToken output', async () => {
const result = parseOpReturn(eTokenInputHex);
expect(result).toStrictEqual(mockParsedETokenOutputArray);
});
test('parseOpReturn() successfully parses an airdrop transaction', async () => {
const result = parseOpReturn(mockAirdropHexOutput);
// verify the hex output is parsed correctly
expect(result).toStrictEqual(mockParsedAirdropMessageArray);
// verify airdrop hex prefix is contained in the array returned from parseOpReturn()
expect(
result.find(
element => element === currency.opReturn.appPrefixesHex.airdrop,
),
).toStrictEqual(currency.opReturn.appPrefixesHex.airdrop);
});
test('convertEtokenToEcashAddr successfully converts a valid eToken address to eCash', async () => {
const result = convertEtokenToEcashAddr(
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
);
expect(result).toStrictEqual(
'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
);
});
test('convertEtokenToEcashAddr successfully converts prefixless eToken address as input', async () => {
const result = convertEtokenToEcashAddr(
'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs',
);
expect(result).toStrictEqual(
'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
);
});
test('convertEtokenToEcashAddr throws error with an invalid eToken address as input', async () => {
const result = convertEtokenToEcashAddr('etoken:qpj9gcldpffcs');
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: etoken:qpj9gcldpffcs is not a valid etoken address',
),
);
});
test('convertEtokenToEcashAddr throws error with an ecash address as input', async () => {
const result = convertEtokenToEcashAddr(
'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8',
);
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8 is not a valid etoken address',
),
);
});
test('convertEtokenToEcashAddr throws error with null input', async () => {
const result = convertEtokenToEcashAddr(null);
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: No etoken address provided',
),
);
});
test('convertEtokenToEcashAddr throws error with empty string input', async () => {
const result = convertEtokenToEcashAddr('');
expect(result).toStrictEqual(
new Error(
'cashMethods.convertToEcashAddr() error: No etoken address provided',
),
);
});
test('convertEcashtoEtokenAddr successfully converts a valid ecash address into an etoken address', async () => {
const eCashAddress = 'ecash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8';
const eTokenAddress =
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs';
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(eTokenAddress);
});
test('convertEcashtoEtokenAddr successfully converts a valid prefix-less ecash address into an etoken address', async () => {
const eCashAddress = 'qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8';
const eTokenAddress =
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs';
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(eTokenAddress);
});
test('convertEcashtoEtokenAddr throws error with invalid ecash address input', async () => {
const eCashAddress = 'ecash:qpaNOTVALIDADDRESSwu8';
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(
new Error(eCashAddress + ' is not a valid ecash address'),
);
});
test('convertEcashtoEtokenAddr throws error with a valid etoken address input', async () => {
const eTokenAddress =
'etoken:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gcldpffcs';
const result = convertEcashtoEtokenAddr(eTokenAddress);
expect(result).toStrictEqual(
new Error(eTokenAddress + ' is not a valid ecash address'),
);
});
test('convertEcashtoEtokenAddr throws error with a valid bitcoincash address input', async () => {
const bchAddress =
'bitcoincash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9g0vsgy56s';
const result = convertEcashtoEtokenAddr(bchAddress);
expect(result).toStrictEqual(
new Error(bchAddress + ' is not a valid ecash address'),
);
});
test('convertEcashtoEtokenPrefix throws error with null ecash address input', async () => {
const eCashAddress = null;
const result = convertEcashtoEtokenAddr(eCashAddress);
expect(result).toStrictEqual(
new Error(eCashAddress + ' is not a valid ecash address'),
);
});
it(`flattenContactList flattens contactList array by returning an array of addresses`, () => {
expect(
flattenContactList([
{
address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr',
name: 'Alpha',
},
{
address: 'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2',
name: 'Beta',
},
{
address: 'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82',
name: 'Gamma',
},
]),
).toStrictEqual([
'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr',
'ecash:qpq235n3l3u6ampc8slapapnatwfy446auuv64ylt2',
'ecash:qz50e58nkeg2ej2f34z6mhwylp6ven8emy8pp52r82',
]);
});
it(`flattenContactList flattens contactList array of length 1 by returning an array of 1 address`, () => {
expect(
flattenContactList([
{
address: 'ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr',
name: 'Alpha',
},
]),
).toStrictEqual(['ecash:qpdkc5p7f25hwkxsr69m3evlj4h7wqq9xcgmjc8sxr']);
});
it(`flattenContactList returns an empty array for invalid input`, () => {
expect(flattenContactList(false)).toStrictEqual([]);
});
it(`getHashArrayFromWallet returns false for a legacy wallet`, () => {
expect(
getHashArrayFromWallet(mockLegacyWallets.legacyAlphaMainnet),
).toBe(false);
});
it(`Successfully extracts a hash160 array from a migrated wallet object`, () => {
expect(
getHashArrayFromWallet(
mockLegacyWallets.migratedLegacyAlphaMainnet,
),
).toStrictEqual([
'960c9ed561f1699f0c49974d50b3bb7cdc118625',
'2be0e0c999e7e77a443ea726f82c441912fca92b',
'ba8257db65f40359989c7b894c5e88ed7b6344f6',
]);
});
it(`isActiveWebsocket returns true for an active chronik websocket connection`, () => {
expect(isActiveWebsocket(activeWebsocketAlpha)).toBe(true);
});
it(`isActiveWebsocket returns false for a disconnected chronik websocket connection`, () => {
expect(isActiveWebsocket(disconnectedWebsocketAlpha)).toBe(false);
});
it(`isActiveWebsocket returns false for a null chronik websocket connection`, () => {
expect(isActiveWebsocket(null)).toBe(false);
});
it(`isActiveWebsocket returns false for an active websocket connection with no subscriptions`, () => {
expect(isActiveWebsocket(unsubscribedWebsocket)).toBe(false);
});
it(`getCashtabByteCount for 2 inputs, 2 outputs returns the same value as BCH.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: p2pkhOutputNumber },
);`, () => {
expect(getCashtabByteCount(2, 2)).toBe(374);
});
it(`getCashtabByteCount for 1 input, 2 outputs returns the same value as BCH.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: p2pkhOutputNumber },
);`, () => {
expect(getCashtabByteCount(1, 2)).toBe(226);
});
it(`getCashtabByteCount for 173 input, 1 outputs returns the same value as BCH.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: p2pkhOutputNumber },
);`, () => {
expect(getCashtabByteCount(173, 1)).toBe(25648);
});
it(`getCashtabByteCount for 1 input, 2000 outputs returns the same value as BCH.BitcoinCash.getByteCount(
{ P2PKH: utxos.length },
{ P2PKH: p2pkhOutputNumber },
);`, () => {
expect(getCashtabByteCount(1, 2000)).toBe(68158);
});
it('calculates fee correctly for 2 P2PKH outputs', () => {
const utxosMock = [{}, {}];
expect(calcFee(utxosMock, 2, 1.01)).toBe(378);
});
it(`Gets correct EC Pair from WIF`, () => {
expect(JSON.stringify(getECPairFromWIF(mockWif))).toBe(
mockStringifiedECPair,
);
});
it(`Converts a hash160 to an ecash address`, () => {
expect(
hash160ToAddress('76458db0ed96fe9863fc1ccec9fa2cfab884b0f6'),
).toBe('ecash:qpmytrdsakt0axrrlswvaj069nat3p9s7cjctmjasj');
});
it(`outputScriptToAddress determines P2PKH address type from output script and returns the ecash address`, () => {
expect(
outputScriptToAddress(
'76a914da45fd71b76e34c88e97ccbebb454d7cd395e52c88ac',
),
).toBe('ecash:qrdytlt3kahrfjywjlxtaw69f47d89099s393kne5c');
});
it(`outputScriptToAddress determines P2SH address type from output script and returns the ecash address`, () => {
expect(
outputScriptToAddress(
'a914c5e60aad8d98f298a76434750630dc1b46a2382187',
),
).toBe('ecash:prz7vz4d3kv09x98vs682p3smsd5dg3cyykjye6grt');
});
it(`outputScriptToAddress throws correct error for an output script that does not parse as P2PKH or P2SH`, () => {
let thrownError;
try {
outputScriptToAddress('notAnOutputScript');
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toBe('Unrecognized outputScript format');
});
it(`outputScriptToAddress throws correct error for an output script that for some reason is bracketed by P2PKH markers but is not a valid hash160`, () => {
let thrownError;
try {
outputScriptToAddress(
'76a914da45fd71b76eeeeeeeee34c88e97ccbebb454d7cd395e52c88ac',
);
} catch (err) {
thrownError = err;
}
expect(thrownError.message).toBe('Parsed hash160 is incorrect length');
});
});
diff --git a/web/cashtab/src/utils/cashMethods.js b/web/cashtab/src/utils/cashMethods.js
index 5a8c5cf04..e1c213296 100644
--- a/web/cashtab/src/utils/cashMethods.js
+++ b/web/cashtab/src/utils/cashMethods.js
@@ -1,1302 +1,1302 @@
import { currency } from 'components/Common/Ticker';
import {
isValidXecAddress,
isValidEtokenAddress,
isValidContactList,
isValidBchAddress,
} from 'utils/validation';
import BigNumber from 'bignumber.js';
import cashaddr from 'ecashaddrjs';
import bs58 from 'bs58';
import * as slpMdm from 'slp-mdm';
import eCash from 'ecashjs-lib';
import coininfo from 'utils/coininfo';
export const getAliasRegistrationFee = alias => {
let registrationFee;
let fee = currency.aliasSettings.aliasRegistrationFeeInSats;
- switch (alias.length) {
+ switch (new Blob([alias]).size) {
case 1:
registrationFee = fee.oneChar;
break;
case 2:
registrationFee = fee.twoChar;
break;
case 3:
registrationFee = fee.threeChar;
break;
case 4:
registrationFee = fee.fourChar;
break;
case 5:
registrationFee = fee.fiveChar;
break;
case 6:
registrationFee = fee.sixChar;
break;
case 7:
registrationFee = fee.sevenChar;
break;
default:
registrationFee = fee.eightChar;
break;
}
return registrationFee;
};
// function is based on BCH-JS' generateBurnOpReturn() however it's been trimmed down for Cashtab use
// Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L217
export const generateBurnOpReturn = (tokenUtxos, burnQty) => {
try {
if (!tokenUtxos || !burnQty) {
throw new Error('Invalid burn token parameter');
} // sendToken component already prevents burning of a value greater than the token utxo total held by the wallet
const tokenId = tokenUtxos[0].tokenId;
const decimals = tokenUtxos[0].decimals;
// account for token decimals
const finalBurnTokenQty = new BigNumber(burnQty).times(10 ** decimals);
// Calculate the total amount of tokens owned by the wallet.
const totalTokens = tokenUtxos.reduce(
(tot, txo) =>
tot.plus(new BigNumber(txo.tokenQty).times(10 ** decimals)),
new BigNumber(0),
);
// calculate the token change
const tokenChange = totalTokens.minus(finalBurnTokenQty);
const tokenChangeStr = tokenChange.toString();
// Generate the burn OP_RETURN as a Buffer
// No need for separate .send() calls for change and non-change burns as
// nil change values do not generate token outputs as the full balance is burnt
const script = slpMdm.TokenType1.send(tokenId, [
new slpMdm.BN(tokenChangeStr),
]);
return script;
} catch (err) {
console.log('Error in generateBurnOpReturn(): ' + err);
throw err;
}
};
// Function originally based on BCH-JS' generateSendOpReturn function however trimmed down for Cashtab
// Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L95
export const generateSendOpReturn = (tokenUtxos, sendQty) => {
try {
if (!tokenUtxos || !sendQty) {
throw new Error('Invalid send token parameter');
}
const tokenId = tokenUtxos[0].tokenId;
const decimals = tokenUtxos[0].decimals;
// account for token decimals
const finalSendTokenQty = new BigNumber(sendQty).times(10 ** decimals);
const finalSendTokenQtyStr = finalSendTokenQty.toString();
// Calculate the total amount of tokens owned by the wallet.
const totalTokens = tokenUtxos.reduce(
(tot, txo) =>
tot.plus(new BigNumber(txo.tokenQty).times(10 ** decimals)),
new BigNumber(0),
);
// calculate token change
const tokenChange = totalTokens.minus(finalSendTokenQty);
const tokenChangeStr = tokenChange.toString();
// When token change output is required
let script, outputs;
if (tokenChange > 0) {
outputs = 2;
// Generate the OP_RETURN as a Buffer.
script = slpMdm.TokenType1.send(tokenId, [
new slpMdm.BN(finalSendTokenQtyStr),
new slpMdm.BN(tokenChangeStr),
]);
} else {
// no token change needed
outputs = 1;
// Generate the OP_RETURN as a Buffer.
script = slpMdm.TokenType1.send(tokenId, [
new slpMdm.BN(finalSendTokenQtyStr),
]);
}
return { script, outputs };
} catch (err) {
console.log('Error in generateSendOpReturn(): ' + err);
throw err;
}
};
// function is based on BCH-JS' generateGenesisOpReturn() however it's been trimmed down for Cashtab use
// Reference: https://github.com/Permissionless-Software-Foundation/bch-js/blob/62e56c832b35731880fe448269818b853c76dd80/src/slp/tokentype1.js#L286
export const generateGenesisOpReturn = configObj => {
try {
if (!configObj) {
throw new Error('Invalid token configuration');
}
// adjust initial quantity for token decimals
const initialQty = new BigNumber(configObj.initialQty)
.times(10 ** configObj.decimals)
.toString();
const script = slpMdm.TokenType1.genesis(
configObj.ticker,
configObj.name,
configObj.documentUrl,
configObj.documentHash,
configObj.decimals,
configObj.mintBatonVout,
new slpMdm.BN(initialQty),
);
return script;
} catch (err) {
console.log('Error in generateGenesisOpReturn(): ' + err);
throw err;
}
};
export const getUtxoWif = (utxo, wallet) => {
if (!wallet) {
throw new Error('Invalid wallet parameter');
}
const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899];
const wif = accounts
.filter(acc => acc.cashAddress === utxo.address)
.pop().fundingWif;
return wif;
};
// Reference https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/ecpair.js#L24
// Modified for mainnet only
export const getECPairFromWIF = wif => {
let xec = coininfo.bitcoincash.main;
const xecBitcoinJSLib = xec.toBitcoinJS();
return eCash.ECPair.fromWIF(wif, xecBitcoinJSLib);
};
export const signUtxosByAddress = (inputUtxos, wallet, txBuilder) => {
for (let i = 0; i < inputUtxos.length; i++) {
const utxo = inputUtxos[i];
const accounts = [wallet.Path245, wallet.Path145, wallet.Path1899];
const wif = accounts
.filter(acc => acc.cashAddress === utxo.address)
.pop().fundingWif;
const utxoECPair = getECPairFromWIF(wif);
txBuilder.sign(
i,
utxoECPair,
undefined,
txBuilder.hashTypes.SIGHASH_ALL,
parseInt(utxo.value),
);
}
return txBuilder;
};
export const getCashtabByteCount = (p2pkhInputCount, p2pkhOutputCount) => {
// Simplifying bch-js function for P2PKH txs only, as this is all Cashtab supports for now
// https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/bitcoincash.js#L408
// The below magic numbers refer to:
// const types = {
// inputs: {
// 'P2PKH': 148 * 4,
// },
// outputs: {
// P2PKH: 34 * 4,
// },
// };
const inputCount = new BigNumber(p2pkhInputCount);
const outputCount = new BigNumber(p2pkhOutputCount);
const inputWeight = new BigNumber(148 * 4);
const outputWeight = new BigNumber(34 * 4);
const nonSegwitWeightConstant = new BigNumber(10 * 4);
let totalWeight = new BigNumber(0);
totalWeight = totalWeight
.plus(inputCount.times(inputWeight))
.plus(outputCount.times(outputWeight))
.plus(nonSegwitWeightConstant);
const byteCount = totalWeight.div(4).integerValue(BigNumber.ROUND_CEIL);
return Number(byteCount);
};
export const calcFee = (
utxos,
p2pkhOutputNumber = 2,
satoshisPerByte = currency.defaultFee,
) => {
const byteCount = getCashtabByteCount(utxos.length, p2pkhOutputNumber);
const txFee = Math.ceil(satoshisPerByte * byteCount);
return txFee;
};
export const generateTokenTxOutput = (
txBuilder,
tokenAction,
legacyCashOriginAddress,
tokenUtxosBeingSpent = [], // optional - send or burn tx only
remainderXecValue = new BigNumber(0), // optional - only if > dust
tokenConfigObj = {}, // optional - genesis only
tokenRecipientAddress = false, // optional - send tx only
tokenAmount = false, // optional - send or burn amount for send/burn tx only
) => {
try {
if (!tokenAction || !legacyCashOriginAddress || !txBuilder) {
throw new Error('Invalid token tx output parameter');
}
let script, opReturnObj, destinationAddress;
switch (tokenAction) {
case 'GENESIS':
script = generateGenesisOpReturn(tokenConfigObj);
destinationAddress = legacyCashOriginAddress;
break;
case 'SEND':
opReturnObj = generateSendOpReturn(
tokenUtxosBeingSpent,
tokenAmount.toString(),
);
script = opReturnObj.script;
destinationAddress = tokenRecipientAddress;
break;
case 'BURN':
script = generateBurnOpReturn(
tokenUtxosBeingSpent,
tokenAmount,
);
destinationAddress = legacyCashOriginAddress;
break;
default:
throw new Error('Invalid token transaction type');
}
// OP_RETURN needs to be the first output in the transaction.
txBuilder.addOutput(script, 0);
// add XEC dust output as fee for genesis, send or burn token output
txBuilder.addOutput(destinationAddress, parseInt(currency.etokenSats));
// Return any token change back to the sender for send and burn txs
if (
tokenAction !== 'GENESIS' ||
(opReturnObj && opReturnObj.outputs > 1)
) {
// add XEC dust output as fee
txBuilder.addOutput(
tokenUtxosBeingSpent[0].address, // etoken address
parseInt(currency.etokenSats),
);
}
// Send xec change to own address
if (remainderXecValue.gte(new BigNumber(currency.dustSats))) {
txBuilder.addOutput(
legacyCashOriginAddress,
parseInt(remainderXecValue),
);
}
} catch (err) {
console.log(`generateTokenTxOutput() error: ` + err);
throw err;
}
return txBuilder;
};
export const generateTxInput = (
isOneToMany,
utxos,
txBuilder,
destinationAddressAndValueArray,
satoshisToSend,
feeInSatsPerByte,
) => {
let txInputObj = {};
const inputUtxos = [];
let txFee = 0;
let totalInputUtxoValue = new BigNumber(0);
try {
if (
(isOneToMany && !destinationAddressAndValueArray) ||
!utxos ||
!txBuilder ||
!satoshisToSend ||
!feeInSatsPerByte
) {
throw new Error('Invalid tx input parameter');
}
// 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
const txOutputs = isOneToMany
? destinationAddressAndValueArray.length + 1
: 2;
for (let i = 0; i < utxos.length; i++) {
const utxo = utxos[i];
totalInputUtxoValue = totalInputUtxoValue.plus(utxo.value);
const vout = utxo.outpoint.outIdx;
const txid = utxo.outpoint.txid;
// add input with txid and index of vout
txBuilder.addInput(txid, vout);
inputUtxos.push(utxo);
txFee = calcFee(inputUtxos, txOutputs, feeInSatsPerByte);
if (totalInputUtxoValue.minus(satoshisToSend).minus(txFee).gte(0)) {
break;
}
}
} catch (err) {
console.log(`generateTxInput() error: ` + err);
throw err;
}
txInputObj.txBuilder = txBuilder;
txInputObj.totalInputUtxoValue = totalInputUtxoValue;
txInputObj.inputUtxos = inputUtxos;
txInputObj.txFee = txFee;
return txInputObj;
};
export const generateTokenTxInput = (
tokenAction, // GENESIS, SEND or BURN
totalXecUtxos,
totalTokenUtxos,
tokenId,
tokenAmount, // optional - only for sending or burning
feeInSatsPerByte,
txBuilder,
) => {
let totalXecInputUtxoValue = new BigNumber(0);
let remainderXecValue = new BigNumber(0);
let remainderTokenValue = new BigNumber(0);
let totalXecInputUtxos = [];
let txFee = 0;
let tokenUtxosBeingSpent = [];
try {
if (
!tokenAction ||
!totalXecUtxos ||
(tokenAction !== 'GENESIS' && !tokenId) ||
!feeInSatsPerByte ||
!txBuilder
) {
throw new Error('Invalid token tx input parameter');
}
// collate XEC UTXOs for this token tx
const txOutputs =
tokenAction === 'GENESIS'
? 2 // one for genesis OP_RETURN output and one for change
: 4; // for SEND/BURN token txs see T2645 on why this is not dynamically generated
for (let i = 0; i < totalXecUtxos.length; i++) {
const thisXecUtxo = totalXecUtxos[i];
totalXecInputUtxoValue = totalXecInputUtxoValue.plus(
new BigNumber(thisXecUtxo.value),
);
const vout = thisXecUtxo.outpoint.outIdx;
const txid = thisXecUtxo.outpoint.txid;
// add input with txid and index of vout
txBuilder.addInput(txid, vout);
totalXecInputUtxos.push(thisXecUtxo);
txFee = calcFee(totalXecInputUtxos, txOutputs, feeInSatsPerByte);
remainderXecValue =
tokenAction === 'GENESIS'
? totalXecInputUtxoValue
.minus(new BigNumber(currency.etokenSats))
.minus(new BigNumber(txFee))
: totalXecInputUtxoValue
.minus(new BigNumber(currency.etokenSats * 2)) // one for token send/burn output, one for token change
.minus(new BigNumber(txFee));
if (remainderXecValue.gte(0)) {
break;
}
}
if (remainderXecValue.lt(0)) {
throw new Error(`Insufficient funds`);
}
let filteredTokenInputUtxos = [];
let finalTokenAmountSpent = new BigNumber(0);
let tokenAmountBeingSpent = new BigNumber(tokenAmount);
if (tokenAction === 'SEND' || tokenAction === 'BURN') {
// filter for token UTXOs matching the token being sent/burnt
filteredTokenInputUtxos = totalTokenUtxos.filter(utxo => {
if (
utxo && // UTXO is associated with a token.
utxo.slpMeta.tokenId === tokenId && // UTXO matches the token ID.
!utxo.slpToken.isMintBaton // UTXO is not a minting baton.
) {
return true;
}
return false;
});
if (filteredTokenInputUtxos.length === 0) {
throw new Error(
'No token UTXOs for the specified token could be found.',
);
}
// collate token UTXOs to cover the token amount being sent/burnt
for (let i = 0; i < filteredTokenInputUtxos.length; i++) {
finalTokenAmountSpent = finalTokenAmountSpent.plus(
new BigNumber(filteredTokenInputUtxos[i].tokenQty),
);
txBuilder.addInput(
filteredTokenInputUtxos[i].outpoint.txid,
filteredTokenInputUtxos[i].outpoint.outIdx,
);
tokenUtxosBeingSpent.push(filteredTokenInputUtxos[i]);
if (tokenAmountBeingSpent.lte(finalTokenAmountSpent)) {
break;
}
}
// calculate token change
remainderTokenValue = finalTokenAmountSpent.minus(
new BigNumber(tokenAmount),
);
if (remainderTokenValue.lt(0)) {
throw new Error(
'Insufficient token UTXOs for the specified token amount.',
);
}
}
} catch (err) {
console.log(`generateTokenTxInput() error: ` + err);
throw err;
}
return {
txBuilder: txBuilder,
inputXecUtxos: totalXecInputUtxos,
inputTokenUtxos: tokenUtxosBeingSpent,
remainderXecValue: remainderXecValue,
remainderTokenValue: remainderTokenValue,
};
};
export const getChangeAddressFromInputUtxos = (inputUtxos, wallet) => {
if (!inputUtxos || !wallet) {
throw new Error('Invalid getChangeAddressFromWallet input parameter');
}
// Assume change address is input address of utxo at index 0
let changeAddress;
// Validate address
try {
changeAddress = inputUtxos[0].address;
if (
!isValidXecAddress(changeAddress) &&
!isValidBchAddress(changeAddress)
) {
throw new Error('Invalid change address');
}
} catch (err) {
throw new Error('Invalid input utxo');
}
return changeAddress;
};
/*
* Parse the total value of a send XEC tx and checks whether it is more than dust
* One to many: isOneToMany is true, singleSendValue is null
* One to one: isOneToMany is false, destinationAddressAndValueArray is null
* Returns the aggregate send value in BigNumber format
*/
export const parseXecSendValue = (
isOneToMany,
singleSendValue,
destinationAddressAndValueArray,
) => {
let value = new BigNumber(0);
try {
if (isOneToMany) {
// this is a one to many XEC transaction
if (
!destinationAddressAndValueArray ||
!destinationAddressAndValueArray.length
) {
throw new Error('Invalid destinationAddressAndValueArray');
}
const arrayLength = destinationAddressAndValueArray.length;
for (let i = 0; i < arrayLength; i++) {
// add the total value being sent in this array of recipients
// each array row is: 'eCash address, send value'
value = BigNumber.sum(
value,
new BigNumber(
destinationAddressAndValueArray[i].split(',')[1],
),
);
}
} else {
// this is a one to one XEC transaction then check singleSendValue
// note: one to many transactions won't be sending a singleSendValue param
if (!singleSendValue) {
throw new Error('Invalid singleSendValue');
}
value = new BigNumber(singleSendValue);
}
// If user is attempting to send an aggregate value that is less than minimum accepted by the backend
if (
value.lt(
new BigNumber(fromSatoshisToXec(currency.dustSats).toString()),
)
) {
// Throw the same error given by the backend attempting to broadcast such a tx
throw new Error('dust');
}
} catch (err) {
console.log('Error in parseXecSendValue: ' + err);
throw err;
}
return value;
};
export const encodeOpReturnScript = scriptChunks => {
// reference https://github.com/Permissionless-Software-Foundation/bch-js/blob/master/src/script.js#L153
const arr = [];
scriptChunks.forEach(chunk => {
arr.push(chunk);
});
return eCash.script.compile(arr);
};
/*
* Generates an OP_RETURN script to reflect the various send XEC permutations
* involving messaging, encryption, eToken IDs and airdrop flags.
*
* Returns the final encoded script object
*/
export const generateOpReturnScript = (
optionalOpReturnMsg,
encryptionFlag,
airdropFlag,
airdropTokenId,
encryptedEj,
optionalAliasRegistrationFlag = false,
) => {
// encrypted mesage is mandatory when encryptionFlag is true
// airdrop token id is mandatory when airdropFlag is true
if ((encryptionFlag && !encryptedEj) || (airdropFlag && !airdropTokenId)) {
throw new Error('Invalid OP RETURN script input');
}
// Note: script.push(Buffer.from(currency.opReturn.opReturnPrefixHex, 'hex')); actually evaluates to '016a'
// instead of keeping the hex string intact. This behavour is specific to the initial script array element.
// To get around this, the bch-js approach of directly using the opReturn prefix in decimal form for the initial entry is used here.
let script = [currency.opReturn.opReturnPrefixDec]; // initialize script with the OP_RETURN op code (6a) in decimal form (106)
try {
if (encryptionFlag) {
// if the user has opted to encrypt this message
// add the encrypted cashtab messaging prefix and encrypted msg to script
script.push(
Buffer.from(
currency.opReturn.appPrefixesHex.cashtabEncrypted,
'hex',
), // 65746162
);
// add the encrypted message to script
script.push(Buffer.from(encryptedEj));
} else {
// this is an un-encrypted message
if (airdropFlag) {
// if this was routed from the airdrop component
// add the airdrop prefix to script
script.push(
Buffer.from(
currency.opReturn.appPrefixesHex.airdrop,
'hex',
), // drop
);
// add the airdrop token ID to script
script.push(Buffer.from(airdropTokenId, 'hex'));
}
if (optionalAliasRegistrationFlag) {
script.push(
Buffer.from(
currency.opReturn.appPrefixesHex.aliasRegistration,
'hex',
), // '.xec'
);
} else {
// add the cashtab prefix to script
script.push(
Buffer.from(
currency.opReturn.appPrefixesHex.cashtab,
'hex',
), // 00746162
);
}
// add the un-encrypted message to script if supplied
if (optionalOpReturnMsg) {
script.push(Buffer.from(optionalOpReturnMsg));
}
}
} catch (err) {
console.log('Error in generateOpReturnScript(): ' + err);
throw err;
}
return encodeOpReturnScript(script);
};
export const generateTxOutput = (
isOneToMany,
singleSendValue,
satoshisToSend,
totalInputUtxoValue,
destinationAddress,
destinationAddressAndValueArray,
changeAddress,
txFee,
txBuilder,
) => {
try {
if (
(isOneToMany && !destinationAddressAndValueArray) ||
(!isOneToMany && !destinationAddress && !singleSendValue) ||
!changeAddress ||
!satoshisToSend ||
!totalInputUtxoValue ||
!txFee ||
!txBuilder
) {
throw new Error('Invalid tx input parameter');
}
// amount to send back to the remainder address.
const remainder = new BigNumber(totalInputUtxoValue)
.minus(satoshisToSend)
.minus(txFee);
if (remainder.lt(0)) {
throw new Error(`Insufficient funds`);
}
if (isOneToMany) {
// for one to many mode, add the multiple outputs from the array
let arrayLength = destinationAddressAndValueArray.length;
for (let i = 0; i < arrayLength; i++) {
// add each send tx from the array as an output
let outputAddress =
destinationAddressAndValueArray[i].split(',')[0];
let outputValue = new BigNumber(
destinationAddressAndValueArray[i].split(',')[1],
);
txBuilder.addOutput(
outputAddress,
parseInt(fromXecToSatoshis(outputValue)),
);
}
} else {
// for one to one mode, add output w/ single address and amount to send
txBuilder.addOutput(
destinationAddress,
parseInt(fromXecToSatoshis(singleSendValue)),
);
}
// if a remainder exists, return to change address as the final output
if (remainder.gte(new BigNumber(currency.dustSats))) {
txBuilder.addOutput(changeAddress, parseInt(remainder));
}
} catch (err) {
console.log('Error in generateTxOutput(): ' + err);
throw err;
}
return txBuilder;
};
export const signAndBuildTx = (inputUtxos, txBuilder, wallet) => {
if (
!inputUtxos ||
inputUtxos.length === 0 ||
!txBuilder ||
!wallet ||
// txBuilder.transaction.tx.ins is empty until the inputUtxos are signed
txBuilder.transaction.tx.outs.length === 0
) {
throw new Error('Invalid buildTx parameter');
}
// Sign each XEC UTXO being consumed and refresh transactionBuilder
txBuilder = signUtxosByAddress(inputUtxos, wallet, txBuilder);
let hex;
try {
// build tx
const tx = txBuilder.build();
// output rawhex
hex = tx.toHex();
} catch (err) {
throw new Error('Transaction build failed');
}
return hex;
};
export function parseOpReturn(hexStr) {
if (
!hexStr ||
typeof hexStr !== 'string' ||
hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex
) {
return false;
}
hexStr = hexStr.slice(2); // remove the first byte i.e. 6a
/*
* @Return: resultArray is structured as follows:
* resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix
* resultArray[1] is the actual cashtab message or the 2nd part of an external message
* resultArray[2 - n] are the additional messages for future protcols
*/
let resultArray = [];
let message = '';
let hexStrLength = hexStr.length;
for (let i = 0; hexStrLength !== 0; i++) {
// part 1: check the preceding byte value for the subsequent message
let byteValue = hexStr.substring(0, 2);
let msgByteSize = 0;
if (byteValue === currency.opReturn.opPushDataOne) {
// if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only
msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10
hexStr = hexStr.slice(4); // strip the 4c + message byte size info
} else {
// take the byte as the message byte size
msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10
hexStr = hexStr.slice(2); // strip the message byte size info
}
// part 2: parse the subsequent message based on bytesize
const msgCharLength = 2 * msgByteSize;
message = hexStr.substring(0, msgCharLength);
if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) {
// add the extracted eToken prefix to array then exit loop
resultArray[i] = currency.opReturn.appPrefixesHex.eToken;
break;
} else if (
i === 0 &&
message === currency.opReturn.appPrefixesHex.cashtab
) {
// add the extracted Cashtab prefix to array
resultArray[i] = currency.opReturn.appPrefixesHex.cashtab;
} else if (
i === 0 &&
message === currency.opReturn.appPrefixesHex.cashtabEncrypted
) {
// add the Cashtab encryption prefix to array
resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted;
} else if (
i === 0 &&
message === currency.opReturn.appPrefixesHex.airdrop
) {
// add the airdrop prefix to array
resultArray[i] = currency.opReturn.appPrefixesHex.airdrop;
// TODO: if i === 1 and message === currency.opReturn.appPrefixesHex.aliasRegistration
// flag accordingly
} else {
// this is either an external message or a subsequent cashtab message loop to extract the message
resultArray[i] = message;
}
// strip out the parsed message
hexStr = hexStr.slice(msgCharLength);
hexStrLength = hexStr.length;
}
return resultArray;
}
export const fromLegacyDecimals = (
amount,
cashDecimals = currency.cashDecimals,
) => {
// Input 0.00000546 BCH
// Output 5.46 XEC or 0.00000546 BCH, depending on currency.cashDecimals
const amountBig = new BigNumber(amount);
const conversionFactor = new BigNumber(10 ** (8 - cashDecimals));
const amountSmallestDenomination = amountBig
.times(conversionFactor)
.toNumber();
return amountSmallestDenomination;
};
export const fromSatoshisToXec = (
amount,
cashDecimals = currency.cashDecimals,
) => {
const amountBig = new BigNumber(amount);
const multiplier = new BigNumber(10 ** (-1 * cashDecimals));
const amountInBaseUnits = amountBig.times(multiplier);
return amountInBaseUnits;
};
export const fromXecToSatoshis = (
sendAmount,
cashDecimals = currency.cashDecimals,
) => {
// Replace the BCH.toSatoshi method with an equivalent function that works for arbitrary decimal places
// Example, for an 8 decimal place currency like Bitcoin
// Input: a BigNumber of the amount of Bitcoin to be sent
// Output: a BigNumber of the amount of satoshis to be sent, or false if input is invalid
// Validate
// Input should be a BigNumber with no more decimal places than cashDecimals
const isValidSendAmount =
BigNumber.isBigNumber(sendAmount) && sendAmount.dp() <= cashDecimals;
if (!isValidSendAmount) {
return false;
}
const conversionFactor = new BigNumber(10 ** cashDecimals);
const sendAmountSmallestDenomination = sendAmount.times(conversionFactor);
return sendAmountSmallestDenomination;
};
export const flattenContactList = contactList => {
/*
Converts contactList from array of objects of type {address: , name: } to array of addresses only
If contact list is invalid, returns and empty array
*/
if (!isValidContactList(contactList)) {
return [];
}
let flattenedContactList = [];
for (let i = 0; i < contactList.length; i += 1) {
const thisAddress = contactList[i].address;
flattenedContactList.push(thisAddress);
}
return flattenedContactList;
};
export const loadStoredWallet = walletStateFromStorage => {
// Accept cached tokens array that does not save BigNumber type of BigNumbers
// Return array with BigNumbers converted
// See BigNumber.js api for how to create a BigNumber object from an object
// https://mikemcl.github.io/bignumber.js/
const liveWalletState =
typeof walletStateFromStorage !== 'undefined'
? walletStateFromStorage
: {};
const keysInLiveWalletState = Object.keys(liveWalletState);
// Newly created wallets may not have a state field
// You only need to do this if you are loading a wallet
// that hasn't yet saved tokens[i].balance as a string
// instead of a BigNumber
if (keysInLiveWalletState.includes('tokens')) {
const { tokens } = liveWalletState;
if (
tokens.length > 0 &&
tokens[0] &&
tokens[0].balance &&
typeof tokens[0].balance !== 'string'
) {
for (let i = 0; i < tokens.length; i += 1) {
const thisTokenBalance = tokens[i].balance;
thisTokenBalance._isBigNumber = true;
tokens[i].balance = new BigNumber(thisTokenBalance);
}
}
}
// Also confirm balance is correct
// Necessary step in case currency.decimals changed since last startup
let nonSlpUtxosToParseForBalance;
let balancesRebased;
if (keysInLiveWalletState.length !== 0) {
if (keysInLiveWalletState.includes('slpBalancesAndUtxos')) {
// If this wallet still includes the wallet.state.slpBalancesAndUtxos field
nonSlpUtxosToParseForBalance =
liveWalletState.slpBalancesAndUtxos.nonSlpUtxos;
} else {
nonSlpUtxosToParseForBalance = liveWalletState.nonSlpUtxos;
}
balancesRebased = getWalletBalanceFromUtxos(
nonSlpUtxosToParseForBalance,
);
} else {
balancesRebased = {
totalBalanceInSatoshis: '0',
totalBalance: '0',
};
}
liveWalletState.balances = balancesRebased;
return liveWalletState;
};
export const getWalletBalanceFromUtxos = nonSlpUtxos => {
const totalBalanceInSatoshis = nonSlpUtxos.reduce(
(previousBalance, utxo) =>
previousBalance.plus(new BigNumber(utxo.value)),
new BigNumber(0),
);
return {
totalBalanceInSatoshis: totalBalanceInSatoshis.toString(),
totalBalance: fromSatoshisToXec(totalBalanceInSatoshis).toString(),
};
};
export const isValidStoredWallet = walletStateFromStorage => {
return (
typeof walletStateFromStorage === 'object' &&
'state' in walletStateFromStorage &&
typeof walletStateFromStorage.state === 'object' &&
'balances' in walletStateFromStorage.state &&
!('hydratedUtxoDetails' in walletStateFromStorage.state) &&
('slpBalancesAndUtxos' in walletStateFromStorage.state ||
('slpUtxos' in walletStateFromStorage.state &&
'nonSlpUtxos' in walletStateFromStorage.state)) &&
'tokens' in walletStateFromStorage.state
);
};
export const getWalletState = wallet => {
if (!wallet || !wallet.state) {
return {
balances: { totalBalance: 0, totalBalanceInSatoshis: 0 },
hydratedUtxoDetails: {},
tokens: [],
slpUtxos: [],
nonSlpUtxos: [],
parsedTxHistory: [],
utxos: [],
};
}
return wallet.state;
};
export function convertEtokenToEcashAddr(eTokenAddress) {
if (!eTokenAddress) {
return new Error(
`cashMethods.convertToEcashAddr() error: No etoken address provided`,
);
}
// Confirm input is a valid eToken address
const isValidInput = isValidEtokenAddress(eTokenAddress);
if (!isValidInput) {
return new Error(
`cashMethods.convertToEcashAddr() error: ${eTokenAddress} is not a valid etoken address`,
);
}
// Check for etoken: prefix
const isPrefixedEtokenAddress = eTokenAddress.slice(0, 7) === 'etoken:';
// If no prefix, assume it is checksummed for an etoken: prefix
const testedEtokenAddr = isPrefixedEtokenAddress
? eTokenAddress
: `etoken:${eTokenAddress}`;
let ecashAddress;
try {
const { type, hash } = cashaddr.decode(testedEtokenAddr);
ecashAddress = cashaddr.encode('ecash', type, hash);
} catch (err) {
return err;
}
return ecashAddress;
}
export function convertToEcashPrefix(bitcoincashPrefixedAddress) {
// Prefix-less addresses may be valid, but the cashaddr.decode function used below
// will throw an error without a prefix. Hence, must ensure prefix to use that function.
const hasPrefix = bitcoincashPrefixedAddress.includes(':');
if (hasPrefix) {
// Is it bitcoincash: or simpleledger:
const { type, hash, prefix } = cashaddr.decode(
bitcoincashPrefixedAddress,
);
let newPrefix;
if (prefix === 'bitcoincash') {
newPrefix = 'ecash';
} else if (prefix === 'simpleledger') {
newPrefix = 'etoken';
} else {
return bitcoincashPrefixedAddress;
}
const convertedAddress = cashaddr.encode(newPrefix, type, hash);
return convertedAddress;
} else {
return bitcoincashPrefixedAddress;
}
}
export function convertEcashtoEtokenAddr(eCashAddress) {
const isValidInput = isValidXecAddress(eCashAddress);
if (!isValidInput) {
return new Error(`${eCashAddress} is not a valid ecash address`);
}
// Check for ecash: prefix
const isPrefixedEcashAddress = eCashAddress.slice(0, 6) === 'ecash:';
// If no prefix, assume it is checksummed for an ecash: prefix
const testedEcashAddr = isPrefixedEcashAddress
? eCashAddress
: `ecash:${eCashAddress}`;
let eTokenAddress;
try {
const { type, hash } = cashaddr.decode(testedEcashAddr);
eTokenAddress = cashaddr.encode('etoken', type, hash);
} catch (err) {
return new Error('eCash to eToken address conversion error');
}
return eTokenAddress;
}
// converts ecash, etoken, bitcoincash and simpleledger addresses to hash160
export function toHash160(addr) {
try {
// decode address hash
const { hash } = cashaddr.decode(addr);
// encode the address hash to legacy format (bitcoin)
const legacyAdress = bs58.encode(hash);
// convert legacy to hash160
const addrHash160 = Buffer.from(bs58.decode(legacyAdress)).toString(
'hex',
);
return addrHash160;
} catch (err) {
console.log('Error converting address to hash160');
throw err;
}
}
/* Converts a serialized buffer containing encrypted data into an object
* that can be interpreted by the ecies-lite library.
*
* For reference on the parsing logic in this function refer to the link below on the segment of
* ecies-lite's encryption function where the encKey, macKey, iv and cipher are sliced and concatenated
* https://github.com/tibetty/ecies-lite/blob/8fd97e80b443422269d0223ead55802378521679/index.js#L46-L55
*
* A similar PSF implmentation can also be found at:
* https://github.com/Permissionless-Software-Foundation/bch-encrypt-lib/blob/master/lib/encryption.js
*
* For more detailed overview on the ecies encryption scheme, see https://cryptobook.nakov.com/asymmetric-key-ciphers/ecies-public-key-encryption
*/
export const convertToEncryptStruct = encryptionBuffer => {
// based on ecies-lite's encryption logic, the encryption buffer is concatenated as follows:
// [ epk + iv + ct + mac ] whereby:
// - The first 32 or 64 chars of the encryptionBuffer is the epk
// - Both iv and ct params are 16 chars each, hence their combined substring is 32 chars from the end of the epk string
// - within this combined iv/ct substring, the first 16 chars is the iv param, and ct param being the later half
// - The mac param is appended to the end of the encryption buffer
// validate input buffer
if (!encryptionBuffer) {
throw new Error(
'cashmethods.convertToEncryptStruct() error: input must be a buffer',
);
}
try {
// variable tracking the starting char position for string extraction purposes
let startOfBuf = 0;
// *** epk param extraction ***
// The first char of the encryptionBuffer indicates the type of the public key
// If the first char is 4, then the public key is 64 chars
// If the first char is 3 or 2, then the public key is 32 chars
// Otherwise this is not a valid encryption buffer compatible with the ecies-lite library
let publicKey;
switch (encryptionBuffer[0]) {
case 4:
publicKey = encryptionBuffer.slice(0, 65); // extract first 64 chars as public key
break;
case 3:
case 2:
publicKey = encryptionBuffer.slice(0, 33); // extract first 32 chars as public key
break;
default:
throw new Error(`Invalid type: ${encryptionBuffer[0]}`);
}
// *** iv and ct param extraction ***
startOfBuf += publicKey.length; // sets the starting char position to the end of the public key (epk) in order to extract subsequent iv and ct substrings
const encryptionTagLength = 32; // the length of the encryption tag (i.e. mac param) computed from each block of ciphertext, and is used to verify no one has tampered with the encrypted data
const ivCtSubstring = encryptionBuffer.slice(
startOfBuf,
encryptionBuffer.length - encryptionTagLength,
); // extract the substring containing both iv and ct params, which is after the public key but before the mac param i.e. the 'encryption tag'
const ivbufParam = ivCtSubstring.slice(0, 16); // extract the first 16 chars of substring as the iv param
const ctbufParam = ivCtSubstring.slice(16); // extract the last 16 chars as substring the ct param
// *** mac param extraction ***
const macParam = encryptionBuffer.slice(
encryptionBuffer.length - encryptionTagLength,
encryptionBuffer.length,
); // extract the mac param appended to the end of the buffer
return {
iv: ivbufParam,
epk: publicKey,
ct: ctbufParam,
mac: macParam,
};
} catch (err) {
console.error(`useBCH.convertToEncryptStruct() error: `, err);
throw err;
}
};
export const isLegacyMigrationRequired = wallet => {
// If the wallet does not have Path1899,
// Or each Path1899, Path145, Path245 does not have a public key
// Then it requires migration
if (
!wallet.Path1899 ||
!wallet.Path1899.publicKey ||
!wallet.Path1899.hash160 ||
!wallet.Path145.publicKey ||
!wallet.Path145.hash160 ||
!wallet.Path245.publicKey ||
!wallet.Path245.hash160
) {
return true;
}
return false;
};
export const getHashArrayFromWallet = wallet => {
// If the wallet has wallet.Path1899.hash160, it's migrated and will have all of them
// Return false for an umigrated wallet
const hash160Array =
wallet && wallet.Path1899 && 'hash160' in wallet.Path1899
? [
wallet.Path245.hash160,
wallet.Path145.hash160,
wallet.Path1899.hash160,
]
: false;
return hash160Array;
};
export const isActiveWebsocket = ws => {
// Return true if websocket is connected and subscribed
// Otherwise return false
return (
ws !== null &&
ws &&
'_ws' in ws &&
'readyState' in ws._ws &&
ws._ws.readyState === 1 &&
'_subs' in ws &&
ws._subs.length > 0
);
};
export const hash160ToAddress = hash160 => {
const buffer = Buffer.from(hash160, 'hex');
// Because ecashaddrjs only accepts Uint8Array as input type, convert
const hash160ArrayBuffer = new ArrayBuffer(buffer.length);
const hash160Uint8Array = new Uint8Array(hash160ArrayBuffer);
for (let i = 0; i < hash160Uint8Array.length; i += 1) {
hash160Uint8Array[i] = buffer[i];
}
// Encode ecash: address
const ecashAddr = cashaddr.encode('ecash', 'P2PKH', hash160Uint8Array);
return ecashAddr;
};
export const outputScriptToAddress = outputScript => {
// returns P2SH or P2PKH address
// P2PKH addresses are in outputScript of type 76a914...88ac
// P2SH addresses are in outputScript of type a914...87
// Return false if cannot determine P2PKH or P2SH address
const typeTestSlice = outputScript.slice(0, 4);
let addressType;
let hash160;
switch (typeTestSlice) {
case '76a9':
addressType = 'P2PKH';
hash160 = outputScript.substring(
outputScript.indexOf('76a914') + '76a914'.length,
outputScript.lastIndexOf('88ac'),
);
break;
case 'a914':
addressType = 'P2SH';
hash160 = outputScript.substring(
outputScript.indexOf('a914') + 'a914'.length,
outputScript.lastIndexOf('87'),
);
break;
default:
throw new Error('Unrecognized outputScript format');
}
// Test hash160 for correct length
if (hash160.length !== 40) {
throw new Error('Parsed hash160 is incorrect length');
}
const buffer = Buffer.from(hash160, 'hex');
// Because ecashaddrjs only accepts Uint8Array as input type, convert
const hash160ArrayBuffer = new ArrayBuffer(buffer.length);
const hash160Uint8Array = new Uint8Array(hash160ArrayBuffer);
for (let i = 0; i < hash160Uint8Array.length; i += 1) {
hash160Uint8Array[i] = buffer[i];
}
// Encode ecash: address
const ecashAddress = cashaddr.encode(
'ecash',
addressType,
hash160Uint8Array,
);
return ecashAddress;
};