You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1785 lines
55 KiB

  1. // Copyright (c) 2018-2020, Zpalmtree
  2. //
  3. // Please see the included LICENSE file for more information.
  4. import * as _ from 'lodash';
  5. import {
  6. Transaction as CreatedTransaction, TransactionOutputs,
  7. Address, Interfaces,
  8. } from 'fedoragold-utils';
  9. import { Config } from './Config';
  10. import { FeeType } from './FeeType';
  11. import { Daemon } from './Daemon';
  12. import { CryptoUtils} from './CnUtils';
  13. import { SubWallets } from './SubWallets';
  14. import { LogCategory, logger, LogLevel } from './Logger';
  15. import {
  16. Transaction as TX, TxInputAndOwner, UnconfirmedInput, PreparedTransactionInfo,
  17. PreparedTransaction
  18. } from './Types';
  19. import {
  20. generateKeyImage, generateKeyDerivation, underivePublicKey,
  21. } from './CryptoWrapper';
  22. import {
  23. getMaxTxSize, prettyPrintAmount, prettyPrintBytes,
  24. splitAmountIntoDenominations, isHex64, estimateTransactionSize,
  25. getMinimumTransactionFee, getTransactionFee,
  26. } from './Utilities';
  27. import {
  28. validateAmount, validateDestinations,
  29. validateIntegratedAddresses, validateMixin, validateOurAddresses,
  30. validatePaymentID,
  31. } from './ValidateParameters';
  32. import {
  33. PRETTY_AMOUNTS, FUSION_TX_MIN_INPUT_COUNT,
  34. FUSION_TX_MIN_IN_OUT_COUNT_RATIO, MAX_FUSION_TX_SIZE,
  35. } from './Constants';
  36. import { SUCCESS, WalletError, WalletErrorCode } from './WalletError';
  37. /**
  38. * Sends a fusion transaction.
  39. * If you need more control, use `sendFusionTransactionAdvanced`
  40. * Note that if your wallet is fully optimized, this will be indicated in the
  41. * returned error code.
  42. *
  43. * @return Returns either [transaction, transaction hash, undefined], or [undefined, undefined, error]
  44. */
  45. export async function sendFusionTransactionBasic(
  46. config: Config,
  47. daemon: Daemon,
  48. subWallets: SubWallets): Promise<PreparedTransactionInfo> {
  49. return sendFusionTransactionAdvanced(
  50. config,
  51. daemon,
  52. subWallets,
  53. );
  54. }
  55. /**
  56. * Sends a transaction, which permits multiple amounts to different destinations,
  57. * specifying the mixin, fee, subwallets to draw funds from, and change address.
  58. *
  59. * All parameters are optional aside from daemon and subWallets.
  60. * @param config
  61. * @param daemon A daemon instance we can send the transaction to
  62. * @param subWallets The subwallets instance to draw funds from
  63. * @param mixin The amount of input keys to hide your input with.
  64. * Your network may enforce a static mixin.
  65. * @param subWalletsToTakeFrom The addresses of the subwallets to draw funds from.
  66. * @param destination The destination for the fusion transactions to be sent to.
  67. * Must be a subwallet in this container.
  68. * @param extraData Extra arbitrary data to include in the transaction
  69. *
  70. * @return Returns either [transaction, transaction hash, undefined], or [undefined, undefined, error]
  71. */
  72. export async function sendFusionTransactionAdvanced(
  73. config: Config,
  74. daemon: Daemon,
  75. subWallets: SubWallets,
  76. mixin?: number,
  77. subWalletsToTakeFrom?: string[],
  78. destination?: string,
  79. extraData?: string): Promise<PreparedTransactionInfo> {
  80. logger.log(
  81. 'Starting sendFusionTransaction process',
  82. LogLevel.DEBUG,
  83. LogCategory.TRANSACTIONS,
  84. );
  85. const returnValue: PreparedTransactionInfo = {
  86. success: false,
  87. error: SUCCESS,
  88. };
  89. if (mixin === undefined) {
  90. mixin = config.mixinLimits.getDefaultMixinByHeight(
  91. daemon.getNetworkBlockCount(),
  92. );
  93. logger.log(
  94. `Mixin not given, defaulting to mixin of ${mixin}`,
  95. LogLevel.DEBUG,
  96. LogCategory.TRANSACTIONS,
  97. );
  98. }
  99. /* Take from all subaddresses if none given */
  100. if (subWalletsToTakeFrom === undefined || subWalletsToTakeFrom.length === 0) {
  101. subWalletsToTakeFrom = subWallets.getAddresses();
  102. logger.log(
  103. `Subwallets to take from not given, defaulting to all subwallets (${subWalletsToTakeFrom})`,
  104. LogLevel.DEBUG,
  105. LogCategory.TRANSACTIONS,
  106. );
  107. }
  108. /* Use primary address as change address if not given */
  109. if (destination === undefined || destination === '') {
  110. destination = subWallets.getPrimaryAddress();
  111. logger.log(
  112. `Destination address not given, defaulting to destination address of ${destination}`,
  113. LogLevel.DEBUG,
  114. LogCategory.TRANSACTIONS,
  115. );
  116. }
  117. logger.log(
  118. 'Prevalidating fusion transaction',
  119. LogLevel.DEBUG,
  120. LogCategory.TRANSACTIONS,
  121. );
  122. /* Verify it's all valid */
  123. const error: WalletError = await validateFusionTransaction(
  124. mixin, subWalletsToTakeFrom, destination,
  125. daemon.getNetworkBlockCount(), subWallets, config,
  126. );
  127. if (!_.isEqual(error, SUCCESS)) {
  128. logger.log(
  129. `Failed to validate fusion transaction: ${error.toString()}`,
  130. LogLevel.DEBUG,
  131. LogCategory.TRANSACTIONS,
  132. );
  133. returnValue.error = error;
  134. return returnValue;
  135. }
  136. /* Get the random inputs for this tx */
  137. const [ourInputs, foundMoney] = await subWallets.getFusionTransactionInputs(
  138. subWalletsToTakeFrom, mixin, daemon.getNetworkBlockCount(),
  139. );
  140. logger.log(
  141. `Selected ${ourInputs.length} inputs for fusion transaction, for total amount of ${prettyPrintAmount(foundMoney)}`,
  142. LogLevel.DEBUG,
  143. LogCategory.TRANSACTIONS,
  144. );
  145. /* Payment ID's are not needed with fusion transactions */
  146. const paymentID: string = '';
  147. /* Fusion transactions are free */
  148. const fee: number = 0;
  149. let fusionTX: CreatedTransaction;
  150. while (true) {
  151. logger.log(
  152. `Verifying fusion transaction is reasonable size`,
  153. LogLevel.DEBUG,
  154. LogCategory.TRANSACTIONS,
  155. );
  156. /* Not enough unspent inputs for a fusion TX, we're fully optimized */
  157. if (ourInputs.length < FUSION_TX_MIN_INPUT_COUNT) {
  158. logger.log(
  159. 'Wallet is fully optimized, cancelling fusion transaction',
  160. LogLevel.DEBUG,
  161. LogCategory.TRANSACTIONS,
  162. );
  163. returnValue.error = new WalletError(WalletErrorCode.FULLY_OPTIMIZED);
  164. return returnValue;
  165. }
  166. /* Amount of the transaction */
  167. const amount = _.sumBy(ourInputs, (input) => input.input.amount);
  168. /* Number of outputs this transaction will create */
  169. const numOutputs = splitAmountIntoDenominations(amount).length;
  170. logger.log(
  171. `Sum of tmp transaction: ${prettyPrintAmount(amount)}, num outputs: ${numOutputs}`,
  172. LogLevel.DEBUG,
  173. LogCategory.TRANSACTIONS,
  174. );
  175. /* Need to have at least 4x more inputs than outputs */
  176. if (numOutputs === 0
  177. || (ourInputs.length / numOutputs) < FUSION_TX_MIN_IN_OUT_COUNT_RATIO) {
  178. logger.log(
  179. `Too many outputs, decreasing number of inputs`,
  180. LogLevel.DEBUG,
  181. LogCategory.TRANSACTIONS,
  182. );
  183. /* Remove last input */
  184. ourInputs.pop();
  185. /* And try again */
  186. continue;
  187. }
  188. const addressesAndAmounts: [string, number][] = [[destination, amount]];
  189. const destinations = await setupDestinations(
  190. addressesAndAmounts,
  191. 0,
  192. destination,
  193. config,
  194. );
  195. const [tx, creationError] = await makeTransaction(
  196. mixin,
  197. fee,
  198. paymentID,
  199. ourInputs,
  200. destinations,
  201. subWallets,
  202. daemon,
  203. config,
  204. extraData
  205. );
  206. if (creationError || tx === undefined) {
  207. logger.log(
  208. `Failed to create fusion transaction, ${(creationError as WalletError).toString()}`,
  209. LogLevel.DEBUG,
  210. LogCategory.TRANSACTIONS,
  211. );
  212. returnValue.error = creationError as WalletError;
  213. return returnValue;
  214. }
  215. if (tx.size > MAX_FUSION_TX_SIZE) {
  216. logger.log(
  217. `Fusion tx is too large, decreasing number of inputs`,
  218. LogLevel.DEBUG,
  219. LogCategory.TRANSACTIONS,
  220. );
  221. /* Transaction too large, remove last input */
  222. ourInputs.pop();
  223. /* And try again */
  224. continue;
  225. }
  226. fusionTX = tx;
  227. /* Creation succeeded, and it's a valid fusion transaction -- lets try
  228. sending it! */
  229. break;
  230. }
  231. logger.log(
  232. `Successfully created fusion transaction, proceeding to validating and sending`,
  233. LogLevel.DEBUG,
  234. LogCategory.TRANSACTIONS,
  235. );
  236. const verifyErr: WalletError = verifyTransaction(
  237. fusionTX,
  238. FeeType.FixedFee(0),
  239. daemon,
  240. config,
  241. );
  242. if (!_.isEqual(verifyErr, SUCCESS)) {
  243. returnValue.error = verifyErr;
  244. return returnValue;
  245. }
  246. const result = await relayTransaction(
  247. fusionTX,
  248. fee,
  249. paymentID,
  250. ourInputs,
  251. destination,
  252. 0,
  253. subWallets,
  254. daemon,
  255. config,
  256. );
  257. const [prettyTransaction, err] = result;
  258. if (err) {
  259. logger.log(
  260. `Failed to verify and send transaction: ${(err as WalletError).toString()}`,
  261. LogLevel.DEBUG,
  262. LogCategory.TRANSACTIONS,
  263. );
  264. returnValue.error = err;
  265. return returnValue;
  266. }
  267. returnValue.success = true;
  268. returnValue.fee = fee;
  269. returnValue.paymentID = paymentID;
  270. returnValue.inputs = ourInputs;
  271. returnValue.changeAddress = destination;
  272. returnValue.changeRequired = 0;
  273. returnValue.rawTransaction = fusionTX;
  274. returnValue.transactionHash = await fusionTX.hash();
  275. returnValue.prettyTransaction = prettyTransaction;
  276. returnValue.destinations = {
  277. nodeFee: undefined, /* technically this line is not needed, will def to undef */
  278. change: undefined,
  279. userDestinations: [{
  280. address: destination,
  281. amount: _.sumBy(ourInputs, (input) => input.input.amount),
  282. }],
  283. };
  284. return returnValue;
  285. }
  286. /**
  287. * Sends a transaction of amount to the address destination, using the
  288. * given payment ID, if specified.
  289. *
  290. * Network fee is set to default, mixin is set to default, all subwallets
  291. * are taken from, primary address is used as change address.
  292. *
  293. * If you need more control, use [[sendTransactionAdvanced]]
  294. *
  295. * @param config
  296. * @param daemon A daemon instance we can send the transaction to
  297. * @param subWallets The subwallets instance to draw funds from
  298. * @param destination The address to send the funds to
  299. * @param amount The amount to send, in ATOMIC units
  300. * @param paymentID The payment ID to include with this transaction. Optional.
  301. * @param relayToNetwork
  302. * @param sendAll
  303. */
  304. export async function sendTransactionBasic(
  305. config: Config,
  306. daemon: Daemon,
  307. subWallets: SubWallets,
  308. destination: string,
  309. amount: number,
  310. paymentID?: string,
  311. relayToNetwork?: boolean,
  312. sendAll?: boolean): Promise<PreparedTransactionInfo> {
  313. return sendTransactionAdvanced(
  314. config,
  315. daemon,
  316. subWallets,
  317. [[destination, amount]],
  318. undefined,
  319. undefined,
  320. paymentID,
  321. undefined,
  322. undefined,
  323. relayToNetwork,
  324. sendAll,
  325. );
  326. }
  327. /**
  328. * Sends a transaction, which permits multiple amounts to different destinations,
  329. * specifying the mixin, fee, subwallets to draw funds from, and change address.
  330. *
  331. * All parameters are optional aside from daemon, subWallets, and addressesAndAmounts.
  332. * @param config
  333. * @param daemon A daemon instance we can send the transaction to
  334. * @param subWallets The subwallets instance to draw funds from
  335. * @param addressesAndAmounts An array of destinations, and amounts to send to that
  336. * destination.
  337. * @param mixin The amount of input keys to hide your input with.
  338. * Your network may enforce a static mixin.
  339. * @param fee The network fee, fee per byte, or minimum fee to use
  340. * with this transaction. Defaults to minimum fee.
  341. * @param paymentID The payment ID to include with this transaction.
  342. * @param subWalletsToTakeFrom The addresses of the subwallets to draw funds from.
  343. * @param changeAddress The address to send any returned change to.
  344. *
  345. * @param relayToNetwork Whether we should submit the transaction to the network or not.
  346. * If set to false, allows you to review the transaction fee before sending it.
  347. * Use [[sendPreparedTransaction]] to send a transaction that you have not
  348. * relayed to the network. Defaults to true.
  349. *
  350. * @param sendAll Whether we should send the entire balance available. Since fee per
  351. * byte means estimating fees is difficult, we can handle that process
  352. * on your behalf. The entire balance minus fees will be sent to the
  353. * first destination address. The amount given in the first destination
  354. * address will be ignored. Any following destinations will have
  355. * the given amount sent. For example, if your destinations array was
  356. * ```
  357. * [['address1', 0], ['address2', 50], ['address3', 100]]
  358. * ```
  359. * Then address2 would be sent 50, address3 would be sent 100,
  360. * and address1 would get whatever remains of the balance
  361. * after paying node/network fees.
  362. * Defaults to false.
  363. * @param extraData Extra arbitrary data to include in the transaction
  364. */
  365. export async function sendTransactionAdvanced(
  366. config: Config,
  367. daemon: Daemon,
  368. subWallets: SubWallets,
  369. addressesAndAmounts: [string, number][],
  370. mixin?: number,
  371. fee?: FeeType,
  372. paymentID?: string,
  373. subWalletsToTakeFrom?: string[],
  374. changeAddress?: string,
  375. relayToNetwork?: boolean,
  376. sendAll?: boolean,
  377. extraData?: string): Promise<PreparedTransactionInfo> {
  378. logger.log(
  379. 'Starting sendTransaction process',
  380. LogLevel.DEBUG,
  381. LogCategory.TRANSACTIONS,
  382. );
  383. const returnValue: PreparedTransactionInfo = {
  384. success: false,
  385. error: SUCCESS,
  386. };
  387. if (mixin === undefined) {
  388. mixin = config.mixinLimits.getDefaultMixinByHeight(
  389. daemon.getNetworkBlockCount(),
  390. );
  391. logger.log(
  392. `Mixin not given, defaulting to mixin of ${mixin}`,
  393. LogLevel.DEBUG,
  394. LogCategory.TRANSACTIONS,
  395. );
  396. }
  397. if (fee === undefined) {
  398. fee = FeeType.MinimumFee(config);
  399. logger.log(
  400. `Fee not given, defaulting to min fee of ${fee.feePerByte} per byte`,
  401. LogLevel.DEBUG,
  402. LogCategory.TRANSACTIONS,
  403. );
  404. }
  405. if (paymentID === undefined) {
  406. paymentID = '';
  407. }
  408. if (subWalletsToTakeFrom === undefined || subWalletsToTakeFrom.length === 0) {
  409. subWalletsToTakeFrom = subWallets.getAddresses();
  410. logger.log(
  411. `Subwallets to take from not given, defaulting to all subwallets (${subWalletsToTakeFrom})`,
  412. LogLevel.DEBUG,
  413. LogCategory.TRANSACTIONS,
  414. );
  415. }
  416. if (changeAddress === undefined || changeAddress === '') {
  417. changeAddress = subWallets.getPrimaryAddress();
  418. logger.log(
  419. `Change address not given, defaulting to change address of ${changeAddress}`,
  420. LogLevel.DEBUG,
  421. LogCategory.TRANSACTIONS,
  422. );
  423. }
  424. if (relayToNetwork === undefined) {
  425. relayToNetwork = true;
  426. logger.log(
  427. `Relay to network not given, defaulting to true`,
  428. LogLevel.DEBUG,
  429. LogCategory.TRANSACTIONS,
  430. );
  431. }
  432. if (sendAll === undefined) {
  433. sendAll = false;
  434. logger.log(
  435. `Send all not given, defaulting to false`,
  436. LogLevel.DEBUG,
  437. LogCategory.TRANSACTIONS,
  438. );
  439. }
  440. const [feeAddress, feeAmount] = daemon.nodeFee();
  441. /* Add the node fee, if it exists */
  442. if (feeAmount !== 0) {
  443. addressesAndAmounts.push([feeAddress, feeAmount]);
  444. logger.log(
  445. `Node fee is not zero, adding node fee of ${
  446. prettyPrintAmount(feeAmount)
  447. } with destination of ${feeAddress}`,
  448. LogLevel.DEBUG,
  449. LogCategory.TRANSACTIONS,
  450. );
  451. }
  452. logger.log(
  453. 'Prevalidating transaction',
  454. LogLevel.DEBUG,
  455. LogCategory.TRANSACTIONS,
  456. );
  457. const error: WalletError = await validateTransaction(
  458. addressesAndAmounts,
  459. mixin,
  460. fee,
  461. paymentID,
  462. subWalletsToTakeFrom,
  463. changeAddress,
  464. sendAll,
  465. daemon.getNetworkBlockCount(),
  466. subWallets,
  467. config,
  468. );
  469. if (!_.isEqual(error, SUCCESS)) {
  470. logger.log(
  471. `Failed to validate transaction: ${error.toString()}`,
  472. LogLevel.DEBUG,
  473. LogCategory.TRANSACTIONS,
  474. );
  475. returnValue.error = error;
  476. return returnValue;
  477. }
  478. for (let [address] of addressesAndAmounts) {
  479. const decoded = await Address.fromAddress(address, config.addressPrefix);
  480. /* Assign payment ID from integrated address if present */
  481. if (decoded.paymentId !== '') {
  482. paymentID = decoded.paymentId;
  483. /* Turn integrated address into standard address */
  484. address = await (await Address.fromPublicKeys(decoded.spend.publicKey, decoded.view.publicKey, undefined, config.addressPrefix)).address();
  485. logger.log(
  486. `Extracted payment ID of ${paymentID} from address ${decoded.address}, resulting non integrated address: ${address}`,
  487. LogLevel.DEBUG,
  488. LogCategory.TRANSACTIONS,
  489. );
  490. }
  491. }
  492. /* Total amount we're sending */
  493. let totalAmount: number = _.sumBy(
  494. addressesAndAmounts, ([, amount]) => amount,
  495. );
  496. const availableInputs: TxInputAndOwner[] = await subWallets.getSpendableTransactionInputs(
  497. subWalletsToTakeFrom,
  498. daemon.getNetworkBlockCount(),
  499. );
  500. let sumOfInputs: number = 0;
  501. const ourInputs: TxInputAndOwner[] = [];
  502. if (fee.isFixedFee) {
  503. logger.log(
  504. `Total amount to send: ${totalAmount}`,
  505. LogLevel.DEBUG,
  506. LogCategory.TRANSACTIONS,
  507. );
  508. totalAmount += fee.fixedFee;
  509. } else {
  510. logger.log(
  511. `Total amount to send (Not including fee per byte): ${totalAmount}`,
  512. LogLevel.DEBUG,
  513. LogCategory.TRANSACTIONS,
  514. );
  515. }
  516. let changeRequired: number = 0;
  517. let requiredAmount: number = totalAmount;
  518. let txResult: [undefined, WalletError] | [CreatedTransaction, undefined] = [ undefined, SUCCESS ];
  519. for (const [i, input] of availableInputs.entries()) {
  520. ourInputs.push(input);
  521. sumOfInputs += input.input.amount;
  522. /* If we're sending all, we want every input, so wait for last iteration */
  523. if (sendAll && i < availableInputs.length - 1) {
  524. continue;
  525. }
  526. if (sumOfInputs >= totalAmount || sendAll) {
  527. logger.log(
  528. `Selected enough inputs (${ourInputs.length}) with sum of ${sumOfInputs} ` +
  529. `to exceed total amount required: ${totalAmount} (not including fee), ` +
  530. `attempting to estimate transaction fee`,
  531. LogLevel.DEBUG,
  532. LogCategory.TRANSACTIONS,
  533. );
  534. /* If sum of inputs is > totalAmount, we need to send some back to
  535. * ourselves */
  536. changeRequired = sumOfInputs - totalAmount;
  537. /* Split transfers up into amounts and keys */
  538. let destinations = await setupDestinations(
  539. addressesAndAmounts,
  540. changeRequired,
  541. changeAddress,
  542. config,
  543. );
  544. /* Using fee per byte, lets take a guess at how large our fee is
  545. * going to be, and then see if we have enough inputs to cover it. */
  546. if (fee.isFeePerByte) {
  547. const transactionSize: number = estimateTransactionSize(
  548. mixin,
  549. ourInputs.length,
  550. destinations.length,
  551. paymentID !== '',
  552. 0,
  553. );
  554. logger.log(
  555. `Estimated transaction size: ${prettyPrintBytes(transactionSize)}`,
  556. LogLevel.DEBUG,
  557. LogCategory.TRANSACTIONS,
  558. );
  559. const estimatedFee: number = getTransactionFee(
  560. transactionSize,
  561. daemon.getNetworkBlockCount(),
  562. fee.feePerByte,
  563. config,
  564. );
  565. logger.log(
  566. `Estimated required transaction fee using fee per byte of ${fee.feePerByte}: ${estimatedFee}`,
  567. LogLevel.DEBUG,
  568. LogCategory.TRANSACTIONS,
  569. );
  570. if (sendAll) {
  571. /* The amount available to be sent to the 1st destination,
  572. * not including fee per byte */
  573. let remainingFunds = sumOfInputs;
  574. /* Remove amounts for fixed destinations. Skipping first
  575. * (send all) target. */
  576. for (let j = 1; j < addressesAndAmounts.length; j++) {
  577. remainingFunds -= addressesAndAmounts[j][1];
  578. }
  579. if (estimatedFee > remainingFunds) {
  580. logger.log(
  581. `Node fee + transaction fee + fixed destinations is greater than available balance`,
  582. LogLevel.DEBUG,
  583. LogCategory.TRANSACTIONS,
  584. );
  585. returnValue.fee = estimatedFee;
  586. returnValue.error = new WalletError(WalletErrorCode.NOT_ENOUGH_BALANCE);
  587. return returnValue;
  588. }
  589. totalAmount = remainingFunds - estimatedFee;
  590. logger.log(
  591. `Sending all, estimated max send minus fees and fixed destinations: ${totalAmount}`,
  592. LogLevel.DEBUG,
  593. LogCategory.TRANSACTIONS,
  594. );
  595. /* Amount to send is sum of inputs (full balance), minus
  596. * node fee, minus estimated fee. */
  597. addressesAndAmounts[0][1] = remainingFunds - estimatedFee;
  598. changeRequired = 0;
  599. destinations = await setupDestinations(
  600. addressesAndAmounts,
  601. changeRequired,
  602. changeAddress,
  603. config,
  604. );
  605. }
  606. let estimatedAmount: number = totalAmount + estimatedFee;
  607. /* Re-add total amount going to fixed destinations */
  608. if (sendAll) {
  609. /* Estimated amount should now equal total balance. */
  610. for (let j = 1; j < addressesAndAmounts.length; j++) {
  611. estimatedAmount += addressesAndAmounts[j][1];
  612. }
  613. }
  614. logger.log(
  615. `Total amount to send (including fee per byte): ${estimatedAmount}`,
  616. LogLevel.DEBUG,
  617. LogCategory.TRANSACTIONS,
  618. );
  619. /* Ok, we have enough inputs to add our estimated fee, lets
  620. * go ahead and try and make the transaction. */
  621. if (sumOfInputs >= estimatedAmount) {
  622. logger.log(
  623. `Selected enough inputs to exceed total amount required, ` +
  624. `attempting to estimate transaction fee`,
  625. LogLevel.DEBUG,
  626. LogCategory.TRANSACTIONS,
  627. );
  628. const [success, result, change, needed] = await tryMakeFeePerByteTransaction(
  629. sumOfInputs,
  630. totalAmount,
  631. estimatedFee,
  632. fee.feePerByte,
  633. addressesAndAmounts,
  634. changeAddress,
  635. mixin,
  636. daemon,
  637. ourInputs,
  638. paymentID,
  639. subWallets,
  640. extraData,
  641. sendAll,
  642. config,
  643. );
  644. if (success) {
  645. txResult = result;
  646. changeRequired = change;
  647. break;
  648. } else {
  649. requiredAmount = needed;
  650. }
  651. } else {
  652. logger.log(
  653. `Did not select enough inputs to exceed total amount required, ` +
  654. `selecting more if available.`,
  655. LogLevel.DEBUG,
  656. LogCategory.TRANSACTIONS,
  657. );
  658. requiredAmount = estimatedAmount;
  659. }
  660. } else {
  661. logger.log(
  662. `Making non fee per byte transaction with fixed fee of ${fee.fixedFee}`,
  663. LogLevel.DEBUG,
  664. LogCategory.TRANSACTIONS,
  665. );
  666. txResult = await makeTransaction(
  667. mixin,
  668. fee.fixedFee,
  669. paymentID as string,
  670. ourInputs,
  671. destinations,
  672. subWallets,
  673. daemon,
  674. config,
  675. extraData
  676. );
  677. const [tx, err] = txResult;
  678. if (err) {
  679. logger.log(
  680. `Error creating transaction, ${err.toString()}`,
  681. LogLevel.DEBUG,
  682. LogCategory.TRANSACTIONS,
  683. );
  684. break;
  685. }
  686. const minFee: number = getMinimumTransactionFee(
  687. tx!.size,
  688. daemon.getNetworkBlockCount(),
  689. config,
  690. );
  691. logger.log(
  692. `Min fee required for generated transaction: ${minFee}`,
  693. LogLevel.DEBUG,
  694. LogCategory.TRANSACTIONS,
  695. );
  696. if (fee.fixedFee >= minFee) {
  697. logger.log(
  698. `Fee of generated transaction is greater than min fee, creation succeeded.`,
  699. LogLevel.DEBUG,
  700. LogCategory.TRANSACTIONS,
  701. );
  702. break;
  703. } else {
  704. logger.log(
  705. `Fee of generated transaction is less than min fee, creation failed.`,
  706. LogLevel.DEBUG,
  707. LogCategory.TRANSACTIONS,
  708. );
  709. returnValue.error = new WalletError(WalletErrorCode.FEE_TOO_SMALL);
  710. return returnValue;
  711. }
  712. }
  713. }
  714. }
  715. if (sumOfInputs < requiredAmount) {
  716. returnValue.fee = requiredAmount - totalAmount;
  717. returnValue.error = new WalletError(WalletErrorCode.NOT_ENOUGH_BALANCE);
  718. logger.log(
  719. `Not enough balance to cover transaction, required: ${requiredAmount}, ` +
  720. `fee: ${returnValue.fee}, available: ${sumOfInputs}`,
  721. LogLevel.DEBUG,
  722. LogCategory.TRANSACTIONS,
  723. );
  724. return returnValue;
  725. }
  726. const [createdTX, creationError] = txResult;
  727. /* Checking for undefined to keep the compiler from complaining later.. */
  728. if (creationError || createdTX === undefined) {
  729. logger.log(
  730. `Failed to create transaction, ${(creationError as WalletError).toString()}`,
  731. LogLevel.DEBUG,
  732. LogCategory.TRANSACTIONS,
  733. );
  734. returnValue.error = creationError as WalletError;
  735. return returnValue;
  736. }
  737. const actualFee: number = sumTransactionFee(createdTX);
  738. logger.log(
  739. `Successfully created transaction, proceeding to validating and sending`,
  740. LogLevel.DEBUG,
  741. LogCategory.TRANSACTIONS,
  742. );
  743. logger.log(
  744. `Created transaction: ${JSON.stringify(createdTX.toString())}`,
  745. LogLevel.TRACE,
  746. LogCategory.TRANSACTIONS,
  747. );
  748. const verifyErr: WalletError = verifyTransaction(
  749. createdTX,
  750. fee,
  751. daemon,
  752. config,
  753. );
  754. if (!_.isEqual(verifyErr, SUCCESS)) {
  755. returnValue.error = verifyErr;
  756. return returnValue;
  757. }
  758. if (relayToNetwork) {
  759. const [prettyTX, err] = await relayTransaction(
  760. createdTX,
  761. actualFee,
  762. paymentID as string,
  763. ourInputs,
  764. changeAddress,
  765. changeRequired,
  766. subWallets,
  767. daemon,
  768. config,
  769. );
  770. if (err) {
  771. logger.log(
  772. `Failed to verify and send transaction: ${(err as WalletError).toString()}`,
  773. LogLevel.DEBUG,
  774. LogCategory.TRANSACTIONS,
  775. );
  776. returnValue.error = err;
  777. return returnValue;
  778. }
  779. returnValue.prettyTransaction = prettyTX;
  780. }
  781. returnValue.success = true;
  782. returnValue.fee = actualFee;
  783. returnValue.paymentID = paymentID;
  784. returnValue.inputs = ourInputs;
  785. returnValue.changeAddress = changeAddress;
  786. returnValue.changeRequired = changeRequired;
  787. returnValue.rawTransaction = createdTX;
  788. returnValue.transactionHash = await createdTX.hash();
  789. returnValue.destinations = {
  790. nodeFee: feeAmount === 0 ? undefined : {
  791. address: feeAddress,
  792. amount: feeAmount,
  793. },
  794. change: changeRequired === 0 ? undefined : {
  795. address: changeAddress,
  796. amount: changeRequired,
  797. },
  798. userDestinations: addressesAndAmounts.map(([address, amount]) => {
  799. return {
  800. address,
  801. amount,
  802. };
  803. }),
  804. };
  805. returnValue.nodeFee = feeAmount;
  806. return returnValue;
  807. }
  808. async function tryMakeFeePerByteTransaction(
  809. sumOfInputs: number,
  810. amountPreFee: number,
  811. estimatedFee: number,
  812. feePerByte: number,
  813. addressesAndAmounts: [string, number][],
  814. changeAddress: string,
  815. mixin: number,
  816. daemon: Daemon,
  817. ourInputs: TxInputAndOwner[],
  818. paymentID: string,
  819. subWallets: SubWallets,
  820. extraData: string = '',
  821. sendAll: boolean,
  822. config: Config): Promise<[
  823. boolean,
  824. ([ CreatedTransaction, undefined ] | [undefined, WalletError ]),
  825. number,
  826. number
  827. ]> {
  828. let attempt: number = 0;
  829. while (true) {
  830. logger.log(
  831. `Attempting fee per byte transaction construction, attempt ${attempt}`,
  832. LogLevel.DEBUG,
  833. LogCategory.TRANSACTIONS,
  834. );
  835. const changeRequired: number = sendAll
  836. ? 0
  837. : sumOfInputs - amountPreFee - estimatedFee;
  838. logger.log(
  839. `Change required: ${changeRequired}`,
  840. LogLevel.DEBUG,
  841. LogCategory.TRANSACTIONS,
  842. );
  843. /* Need to recalculate destinations since amount of change, err, changed! */
  844. const destinations = await setupDestinations(
  845. addressesAndAmounts,
  846. changeRequired,
  847. changeAddress,
  848. config,
  849. );
  850. const result = await makeTransaction(
  851. mixin,
  852. estimatedFee,
  853. paymentID,
  854. ourInputs,
  855. destinations,
  856. subWallets,
  857. daemon,
  858. config,
  859. extraData
  860. );
  861. const [ tx, creationError ] = result;
  862. if (creationError) {
  863. logger.log(
  864. `Error creating transaction, ${creationError.toString()}`,
  865. LogLevel.DEBUG,
  866. LogCategory.TRANSACTIONS,
  867. );
  868. return [ true, result, 0, 0 ];
  869. }
  870. const actualTxSize = tx!.size;
  871. logger.log(
  872. `Size of generated transaction: ${prettyPrintBytes(actualTxSize)}`,
  873. LogLevel.DEBUG,
  874. LogCategory.TRANSACTIONS,
  875. );
  876. const requiredFee: number = getTransactionFee(
  877. actualTxSize,
  878. daemon.getNetworkBlockCount(),
  879. feePerByte,
  880. config,
  881. );
  882. logger.log(
  883. `Required transaction fee using fee per byte of ${feePerByte}: ${requiredFee}`,
  884. LogLevel.DEBUG,
  885. LogCategory.TRANSACTIONS,
  886. );
  887. /* Great! The fee we estimated is greater than or equal
  888. * to the min/specified fee per byte for a transaction
  889. * of this size, so we can continue with sending the
  890. * transaction. */
  891. if (estimatedFee >= requiredFee) {
  892. logger.log(
  893. `Estimated fee of ${estimatedFee} is greater ` +
  894. `than or equal to required fee of ${requiredFee}, creation succeeded.`,
  895. LogLevel.DEBUG,
  896. LogCategory.TRANSACTIONS,
  897. );
  898. return [ true, result, changeRequired, 0 ];
  899. }
  900. logger.log(
  901. `Estimated fee of ${estimatedFee} is less` +
  902. `than required fee of ${requiredFee}.`,
  903. LogLevel.DEBUG,
  904. LogCategory.TRANSACTIONS,
  905. );
  906. /* If we're sending all, then we adjust the amount we're sending,
  907. * rather than the change we're returning. */
  908. if (sendAll) {
  909. /* Update the amount we're sending, by readding the too small fee,
  910. * and taking off the requiredFee. I.e., if estimated was 35,
  911. * required was 40, then we'd end up sending 5 less to the destination
  912. * to cover the new fee required. */
  913. addressesAndAmounts[0][1] = addressesAndAmounts[0][1] + estimatedFee - requiredFee;
  914. estimatedFee = requiredFee;
  915. logger.log(
  916. `Sending all, adjusting primary transaction amount down to ${addressesAndAmounts[0][1]}`,
  917. LogLevel.DEBUG,
  918. LogCategory.TRANSACTIONS,
  919. );
  920. }
  921. /* The actual fee required for a tx of this size is not
  922. * covered by the amount of inputs we have so far, lets
  923. * go select some more then try again. */
  924. if (amountPreFee + requiredFee > sumOfInputs) {
  925. logger.log(
  926. `Do not have enough inputs selected to cover required fee. Returning ` +
  927. `to select more inputs if available.`,
  928. LogLevel.DEBUG,
  929. LogCategory.TRANSACTIONS,
  930. );
  931. return [ false, result, changeRequired, amountPreFee + requiredFee ];
  932. }
  933. logger.log(
  934. `Updating estimated fee to ${requiredFee} and attempting transaction ` +
  935. `construction again.`,
  936. LogLevel.DEBUG,
  937. LogCategory.TRANSACTIONS,
  938. );
  939. attempt++;
  940. }
  941. }
  942. export async function sendPreparedTransaction(
  943. transaction: PreparedTransaction,
  944. subWallets: SubWallets,
  945. daemon: Daemon,
  946. config: Config): Promise<PreparedTransactionInfo> {
  947. const returnValue: PreparedTransactionInfo = {
  948. success: false,
  949. error: SUCCESS,
  950. ...transaction,
  951. };
  952. for (const input of transaction.inputs) {
  953. if (!subWallets.haveSpendableInput(input.input, daemon.getNetworkBlockCount())) {
  954. logger.log(
  955. `Prepared transaction ${transaction.rawTransaction.hash} expired, input ${input.input.key}`,
  956. LogLevel.DEBUG,
  957. LogCategory.TRANSACTIONS,
  958. );
  959. returnValue.error = new WalletError(WalletErrorCode.PREPARED_TRANSACTION_EXPIRED);
  960. return returnValue;
  961. }
  962. }
  963. const [prettyTX, err] = await relayTransaction(
  964. transaction.rawTransaction,
  965. transaction.fee,
  966. transaction.paymentID,
  967. transaction.inputs,
  968. transaction.changeAddress,
  969. transaction.changeRequired,
  970. subWallets,
  971. daemon,
  972. config,
  973. );
  974. if (err) {
  975. logger.log(
  976. `Failed to verify and send transaction: ${(err as WalletError).toString()}`,
  977. LogLevel.DEBUG,
  978. LogCategory.TRANSACTIONS,
  979. );
  980. returnValue.error = err;
  981. return returnValue;
  982. }
  983. returnValue.prettyTransaction = prettyTX;
  984. returnValue.success = true;
  985. return returnValue;
  986. }
  987. async function setupDestinations(
  988. addressesAndAmountsTmp: [string, number][],
  989. changeRequired: number,
  990. changeAddress: string,
  991. config: Config): Promise<Interfaces.GeneratedOutput[]> {
  992. /* Clone array so we don't manipulate it outside the function */
  993. const addressesAndAmounts: [string, number][] = addressesAndAmountsTmp.slice();
  994. if (changeRequired !== 0) {
  995. addressesAndAmounts.push([changeAddress, changeRequired]);
  996. }
  997. let amounts: [string, number][] = [];
  998. /* Split amounts into denominations */
  999. addressesAndAmounts.map(([address, amount]) => {
  1000. for (const denomination of splitAmountIntoDenominations(amount)) {
  1001. amounts.push([address, denomination]);
  1002. }
  1003. });
  1004. logger.log(
  1005. `Split destinations into ${amounts.length} outputs`,
  1006. LogLevel.DEBUG,
  1007. LogCategory.TRANSACTIONS,
  1008. );
  1009. amounts = _.sortBy(amounts, ([, amount]) => amount);
  1010. /* Prepare destinations keys */
  1011. const result: Interfaces.GeneratedOutput[] = [];
  1012. for (const [address, amount] of amounts) {
  1013. result.push({
  1014. amount: amount,
  1015. destination: await Address.fromAddress(address, config.addressPrefix)
  1016. })
  1017. }
  1018. return result;
  1019. }
  1020. async function makeTransaction(
  1021. mixin: number,
  1022. fee: number,
  1023. paymentID: string,
  1024. ourInputs: TxInputAndOwner[],
  1025. destinations: Interfaces.GeneratedOutput[],
  1026. subWallets: SubWallets,
  1027. daemon: Daemon,
  1028. config: Config,
  1029. extraData?: string): Promise<([CreatedTransaction, undefined]) | ([undefined, WalletError])> {
  1030. ourInputs = _.sortBy(ourInputs, (input) => input.input.amount);
  1031. logger.log(
  1032. `Collecting ring participants`,
  1033. LogLevel.DEBUG,
  1034. LogCategory.TRANSACTIONS,
  1035. );
  1036. const randomOuts: WalletError | Interfaces.RandomOutput[][] = await getRingParticipants(
  1037. ourInputs, mixin, daemon, config,
  1038. );
  1039. if (randomOuts instanceof WalletError) {
  1040. logger.log(
  1041. `Failed to get ring participants: ${randomOuts.toString()}`,
  1042. LogLevel.DEBUG,
  1043. LogCategory.TRANSACTIONS,
  1044. );
  1045. return [undefined, randomOuts as WalletError];
  1046. }
  1047. let numPregenerated = 0;
  1048. let numGeneratedOnDemand = 0;
  1049. const ourOutputs: Interfaces.Output[] = await Promise.all(ourInputs.map(async (input) => {
  1050. if (!input.input.privateEphemeral || !isHex64(input.input.privateEphemeral)) {
  1051. const [, tmpSecretKey] = await generateKeyImage(
  1052. input.input.transactionPublicKey,
  1053. subWallets.getPrivateViewKey(),
  1054. input.publicSpendKey,
  1055. input.privateSpendKey,
  1056. input.input.transactionIndex,
  1057. config,
  1058. );
  1059. input.input.privateEphemeral = tmpSecretKey;
  1060. numGeneratedOnDemand++;
  1061. } else {
  1062. numPregenerated++;
  1063. }
  1064. return {
  1065. amount: input.input.amount,
  1066. globalIndex: input.input.globalOutputIndex as number,
  1067. index: input.input.transactionIndex,
  1068. input: {
  1069. privateEphemeral: input.input.privateEphemeral,
  1070. publicEphemeral: '', // Required by compiler, not used in func
  1071. transactionKeys: {
  1072. derivedKey: '',
  1073. outputIndex: input.input.transactionIndex,
  1074. publicKey: input.input.transactionPublicKey,
  1075. },
  1076. },
  1077. key: input.input.key,
  1078. keyImage: input.input.keyImage,
  1079. };
  1080. }));
  1081. logger.log(
  1082. `Generated key images for ${numGeneratedOnDemand} inputs, used pre-generated key images for ${numPregenerated} inputs.`,
  1083. LogLevel.DEBUG,
  1084. LogCategory.TRANSACTIONS,
  1085. );
  1086. try {
  1087. logger.log(
  1088. `Asynchronously creating transaction with fedoragold-utils`,
  1089. LogLevel.DEBUG,
  1090. LogCategory.TRANSACTIONS,
  1091. );
  1092. const tx = await CryptoUtils(config).createTransaction(
  1093. destinations, ourOutputs, randomOuts as Interfaces.RandomOutput[][], mixin, fee,
  1094. paymentID, undefined, extraData
  1095. );
  1096. logger.log(
  1097. `Transaction creation succeeded`,
  1098. LogLevel.DEBUG,
  1099. LogCategory.TRANSACTIONS,
  1100. );
  1101. return [tx, undefined];
  1102. } catch (err) {
  1103. if (err instanceof Error)
  1104. logger.log(
  1105. `Error while creating transaction with fedoragold-utils: ${err.toString()}`,
  1106. LogLevel.DEBUG,
  1107. LogCategory.TRANSACTIONS,
  1108. );
  1109. let strErr = "Error in makeTransaction";
  1110. if (err instanceof Error) strErr = err.toString();
  1111. return [undefined, new WalletError(WalletErrorCode.UNKNOWN_ERROR, strErr)];
  1112. }
  1113. }
  1114. function verifyTransaction(
  1115. tx: CreatedTransaction,
  1116. fee: FeeType,
  1117. daemon: Daemon,
  1118. config: Config): WalletError {
  1119. logger.log(
  1120. 'Verifying size of transaction',
  1121. LogLevel.DEBUG,
  1122. LogCategory.TRANSACTIONS,
  1123. );
  1124. /* Check the transaction isn't too large to fit in a block */
  1125. const tooBigErr: WalletError = isTransactionPayloadTooBig(
  1126. tx.size, daemon.getNetworkBlockCount(), config,
  1127. );
  1128. if (!_.isEqual(tooBigErr, SUCCESS)) {
  1129. return tooBigErr;
  1130. }
  1131. logger.log(
  1132. 'Verifying amounts of transaction',
  1133. LogLevel.DEBUG,
  1134. LogCategory.TRANSACTIONS,
  1135. );
  1136. /* Check all the output amounts are members of 'PRETTY_AMOUNTS', otherwise
  1137. they will not be mixable */
  1138. if (!verifyAmounts(tx.outputs)) {
  1139. return new WalletError(WalletErrorCode.AMOUNTS_NOT_PRETTY);
  1140. }
  1141. logger.log(
  1142. 'Verifying transaction fee',
  1143. LogLevel.DEBUG,
  1144. LogCategory.TRANSACTIONS,
  1145. );
  1146. /* Check the transaction has the fee that we expect (0 for fusion) */
  1147. if (!verifyTransactionFee(tx.size, fee, sumTransactionFee(tx))) {
  1148. return new WalletError(WalletErrorCode.UNEXPECTED_FEE);
  1149. }
  1150. return SUCCESS;
  1151. }
  1152. async function relayTransaction(
  1153. tx: CreatedTransaction,
  1154. fee: number,
  1155. paymentID: string,
  1156. inputs: TxInputAndOwner[],
  1157. changeAddress: string,
  1158. changeRequired: number,
  1159. subWallets: SubWallets,
  1160. daemon: Daemon,
  1161. config: Config): Promise<[TX, undefined] | [undefined, WalletError]> {
  1162. logger.log(
  1163. 'Relaying transaction',
  1164. LogLevel.DEBUG,
  1165. LogCategory.TRANSACTIONS,
  1166. );
  1167. const error = await daemon.sendTransaction(tx.toString());
  1168. if (!_.isEqual(error, SUCCESS)) {
  1169. logger.log(
  1170. `Failed to relay transaction. ${error}`,
  1171. LogLevel.DEBUG,
  1172. LogCategory.TRANSACTIONS,
  1173. );
  1174. return [undefined, error];
  1175. }
  1176. logger.log(
  1177. 'Storing sent transaction',
  1178. LogLevel.DEBUG,
  1179. LogCategory.TRANSACTIONS,
  1180. );
  1181. /* Store the unconfirmed transaction, update our balance */
  1182. const returnTX: TX = await storeSentTransaction(
  1183. await tx.hash(), tx.outputs, tx.transactionKeys.publicKey,
  1184. fee, paymentID, inputs, subWallets, config,
  1185. );
  1186. logger.log(
  1187. 'Marking sent inputs as locked',
  1188. LogLevel.DEBUG,
  1189. LogCategory.TRANSACTIONS,
  1190. );
  1191. /* Lock the input for spending till confirmed/cancelled */
  1192. for (const input of inputs) {
  1193. subWallets.markInputAsLocked(input.publicSpendKey, input.input.keyImage);
  1194. }
  1195. logger.log(
  1196. 'Transaction process complete.',
  1197. LogLevel.DEBUG,
  1198. LogCategory.TRANSACTIONS,
  1199. );
  1200. return [returnTX, undefined];
  1201. }
  1202. async function storeSentTransaction(
  1203. hash: string,
  1204. keyOutputs: TransactionOutputs.ITransactionOutput[],
  1205. txPublicKey: string,
  1206. fee: number,
  1207. paymentID: string,
  1208. ourInputs: TxInputAndOwner[],
  1209. subWallets: SubWallets,
  1210. config: Config): Promise<TX> {
  1211. const transfers: Map<string, number> = new Map();
  1212. const derivation: string = await generateKeyDerivation(
  1213. txPublicKey, subWallets.getPrivateViewKey(), config,
  1214. );
  1215. const spendKeys: string[] = subWallets.getPublicSpendKeys();
  1216. for (const [outputIndex, output] of keyOutputs.entries()) {
  1217. if (output.type === TransactionOutputs.OutputType.KEY) {
  1218. const o = output as TransactionOutputs.KeyOutput;
  1219. /* Derive the spend key from the transaction, using the previous
  1220. derivation */
  1221. const derivedSpendKey = await underivePublicKey(
  1222. derivation, outputIndex, o.key, config,
  1223. );
  1224. /* See if the derived spend key matches any of our spend keys */
  1225. if (!_.includes(spendKeys, derivedSpendKey)) {
  1226. continue;
  1227. }
  1228. const input: UnconfirmedInput = new UnconfirmedInput(
  1229. o.amount.toJSNumber(), o.key, hash,
  1230. );
  1231. subWallets.storeUnconfirmedIncomingInput(input, derivedSpendKey);
  1232. transfers.set(
  1233. derivedSpendKey,
  1234. o.amount.add(transfers.get(derivedSpendKey) || 0).toJSNumber(),
  1235. );
  1236. }
  1237. }
  1238. for (const input of ourInputs) {
  1239. /* Amounts we have spent, subtract them from the transfers map */
  1240. transfers.set(
  1241. input.publicSpendKey,
  1242. -input.input.amount + (transfers.get(input.publicSpendKey) || 0),
  1243. );
  1244. }
  1245. const timestamp: number = 0;
  1246. const blockHeight: number = 0;
  1247. const unlockTime: number = 0;
  1248. const isCoinbaseTransaction: boolean = false;
  1249. const tx: TX = new TX(
  1250. transfers, hash, fee, timestamp, blockHeight, paymentID,
  1251. unlockTime, isCoinbaseTransaction,
  1252. );
  1253. subWallets.addUnconfirmedTransaction(tx);
  1254. logger.log(
  1255. `Stored unconfirmed transaction: ${JSON.stringify(tx)}`,
  1256. LogLevel.TRACE,
  1257. LogCategory.TRANSACTIONS,
  1258. );
  1259. return tx;
  1260. }
  1261. /**
  1262. * Verify the transaction is small enough to fit in a block
  1263. */
  1264. function isTransactionPayloadTooBig(
  1265. txSize: number,
  1266. currentHeight: number,
  1267. config: Config): WalletError {
  1268. const maxTxSize: number = getMaxTxSize(currentHeight, config.blockTargetTime);
  1269. if (txSize > maxTxSize) {
  1270. return new WalletError(
  1271. WalletErrorCode.TOO_MANY_INPUTS_TO_FIT_IN_BLOCK,
  1272. `Transaction is too large: (${prettyPrintBytes(txSize)}). Max ` +
  1273. `allowed size is ${prettyPrintBytes(maxTxSize)}. Decrease the ` +
  1274. `amount you are sending, or perform some fusion transactions.`,
  1275. );
  1276. }
  1277. return SUCCESS;
  1278. }
  1279. /**
  1280. * Verify all the output amounts are members of PRETTY_AMOUNTS, otherwise they
  1281. * will not be mixable
  1282. */
  1283. function verifyAmounts(amounts: TransactionOutputs.ITransactionOutput[]): boolean {
  1284. for (const output of amounts) {
  1285. if (output.type === TransactionOutputs.OutputType.KEY) {
  1286. if (!PRETTY_AMOUNTS.includes((output as TransactionOutputs.KeyOutput).amount.toJSNumber())) {
  1287. return false;
  1288. }
  1289. }
  1290. }
  1291. return true;
  1292. }
  1293. function sumTransactionFee(transaction: CreatedTransaction): number {
  1294. const inputTotal: number = transaction.amount;
  1295. let outputTotal: number = 0;
  1296. for (const output of transaction.outputs) {
  1297. if (output.type === TransactionOutputs.OutputType.KEY) {
  1298. outputTotal += (output as TransactionOutputs.KeyOutput).amount.toJSNumber();
  1299. }
  1300. }
  1301. return inputTotal - outputTotal;
  1302. }
  1303. /**
  1304. * Verify the transaction fee is the same as the requested transaction fee
  1305. */
  1306. function verifyTransactionFee(
  1307. transactionSize: number,
  1308. expectedFee: FeeType,
  1309. actualFee: number): boolean {
  1310. if (expectedFee.isFixedFee) {
  1311. return expectedFee.fixedFee === actualFee;
  1312. } else {
  1313. const calculatedFee: number = expectedFee.feePerByte * transactionSize;
  1314. /* Ensure fee is greater or equal to the fee per byte specified,
  1315. * and no more than two times the fee per byte specified. */
  1316. return actualFee >= calculatedFee && actualFee <= calculatedFee * 2;
  1317. }
  1318. }
  1319. /**
  1320. * Get sufficient random outputs for the transaction. Returns an error if
  1321. * can't get outputs or can't get enough outputs.
  1322. */
  1323. async function getRingParticipants(
  1324. inputs: TxInputAndOwner[],
  1325. mixin: number,
  1326. daemon: Daemon,
  1327. config: Config): Promise<WalletError | Interfaces.RandomOutput[][]> {
  1328. if (mixin === 0) {
  1329. logger.log(
  1330. `Mixin = 0, no ring participants needed`,
  1331. LogLevel.DEBUG,
  1332. LogCategory.TRANSACTIONS,
  1333. );
  1334. return [];
  1335. }
  1336. /* Request one more than needed, this way if we get our own output as
  1337. one of the mixin outs, we can skip it and still form the transaction */
  1338. const requestedOuts: number = mixin + 1;
  1339. const amounts: number[] = inputs.map((input) => input.input.amount);
  1340. const outs = await daemon.getRandomOutputsByAmount(amounts, requestedOuts);
  1341. if (outs.length === 0) {
  1342. logger.log(
  1343. `Failed to get any random outputs from the daemon`,
  1344. LogLevel.DEBUG,
  1345. LogCategory.TRANSACTIONS,
  1346. );
  1347. return new WalletError(WalletErrorCode.DAEMON_OFFLINE);
  1348. }
  1349. for (const amount of amounts) {
  1350. /* Check each amount is present in outputs */
  1351. const foundOutputs = _.find(outs, ([outAmount, ]) => amount === outAmount);
  1352. if (foundOutputs === undefined) {
  1353. return new WalletError(
  1354. WalletErrorCode.NOT_ENOUGH_FAKE_OUTPUTS,
  1355. `Failed to get any matching outputs for amount ${amount} ` +
  1356. `(${prettyPrintAmount(amount, config)}). Further explanation here: ` +
  1357. `https://gist.github.com/zpalmtree/80b3e80463225bcfb8f8432043cb594c`,
  1358. );
  1359. }
  1360. const [, outputs] = foundOutputs;
  1361. if (outputs.length < mixin) {
  1362. return new WalletError(
  1363. WalletErrorCode.NOT_ENOUGH_FAKE_OUTPUTS,
  1364. `Failed to get enough matching outputs for amount ${amount} ` +
  1365. `(${prettyPrintAmount(amount, config)}). Needed outputs: ${mixin} ` +
  1366. `, found outputs: ${outputs.length}. Further explanation here: ` +
  1367. `https://gist.github.com/zpalmtree/80b3e80463225bcfb8f8432043cb594c`,
  1368. );
  1369. }
  1370. }
  1371. if (outs.length !== amounts.length) {
  1372. return new WalletError(WalletErrorCode.NOT_ENOUGH_FAKE_OUTPUTS);
  1373. }
  1374. const randomOuts: Interfaces.RandomOutput[][] = [];
  1375. /* Do the same check as above here, again. The reason being that
  1376. we just find the first set of outputs matching the amount above,
  1377. and if we requests, say, outputs for the amount 100 twice, the
  1378. first set might be sufficient, but the second are not.
  1379. We could just check here instead of checking above, but then we
  1380. might hit the length message first. Checking this way gives more
  1381. informative errors. */
  1382. for (const [amount, outputs] of outs) {
  1383. if (outputs.length < mixin) {
  1384. return new WalletError(
  1385. WalletErrorCode.NOT_ENOUGH_FAKE_OUTPUTS,
  1386. `Failed to get enough matching outputs for amount ${amount} ` +
  1387. `(${prettyPrintAmount(amount, config)}). Needed outputs: ${mixin} ` +
  1388. `, found outputs: ${outputs.length}. Further explanation here: ` +
  1389. `https://gist.github.com/zpalmtree/80b3e80463225bcfb8f8432043cb594c`,
  1390. );
  1391. }
  1392. randomOuts.push(outputs.map(([index, key]) => {
  1393. return {
  1394. globalIndex: index,
  1395. key: key,
  1396. };
  1397. }));
  1398. }
  1399. logger.log(
  1400. `Finished gathering ring participants`,
  1401. LogLevel.DEBUG,
  1402. LogCategory.TRANSACTIONS,
  1403. );
  1404. return randomOuts;
  1405. }
  1406. /**
  1407. * Validate the given transaction parameters are valid.
  1408. *
  1409. * @return Returns either SUCCESS or an error representing the issue
  1410. */
  1411. async function validateTransaction(
  1412. destinations: [string, number][],
  1413. mixin: number,
  1414. fee: FeeType,
  1415. paymentID: string,
  1416. subWalletsToTakeFrom: string[],
  1417. changeAddress: string,
  1418. sendAll: boolean,
  1419. currentHeight: number,
  1420. subWallets: SubWallets,
  1421. config: Config): Promise<WalletError> {
  1422. /* Validate the destinations are valid */
  1423. let error: WalletError = await validateDestinations(destinations, config);
  1424. if (!_.isEqual(error, SUCCESS)) {
  1425. return error;
  1426. }
  1427. /* Validate stored payment ID's in integrated addresses don't conflict */
  1428. error = await validateIntegratedAddresses(destinations, paymentID, config);
  1429. if (!_.isEqual(error, SUCCESS)) {
  1430. return error;
  1431. }
  1432. /* Verify the subwallets to take from exist */
  1433. error = await validateOurAddresses(subWalletsToTakeFrom, subWallets, config);
  1434. if (!_.isEqual(error, SUCCESS)) {
  1435. return error;
  1436. }
  1437. /* Verify we have enough money for the transaction */
  1438. error = await validateAmount(destinations, fee, subWalletsToTakeFrom, subWallets, currentHeight, config);
  1439. if (!_.isEqual(error, SUCCESS)) {
  1440. return error;
  1441. }
  1442. /* Validate mixin is within the bounds for the current height */
  1443. error = validateMixin(mixin, currentHeight, config);
  1444. if (!_.isEqual(error, SUCCESS)) {
  1445. return error;
  1446. }
  1447. error = validatePaymentID(paymentID);
  1448. if (!_.isEqual(error, SUCCESS)) {
  1449. return error;
  1450. }
  1451. error = await validateOurAddresses([changeAddress], subWallets, config);
  1452. if (!_.isEqual(error, SUCCESS)) {
  1453. return error;
  1454. }
  1455. return SUCCESS;
  1456. }
  1457. /**
  1458. * Validate the given transaction parameters are valid.
  1459. *
  1460. * @return Returns either SUCCESS or an error representing the issue
  1461. */
  1462. async function validateFusionTransaction(
  1463. mixin: number,
  1464. subWalletsToTakeFrom: string[],
  1465. destination: string,
  1466. currentHeight: number,
  1467. subWallets: SubWallets,
  1468. config: Config): Promise<WalletError> {
  1469. /* Validate mixin is within the bounds for the current height */
  1470. let error: WalletError = validateMixin(mixin, currentHeight, config);
  1471. if (_.isEqual(error, SUCCESS)) {
  1472. return error;
  1473. }
  1474. /* Verify the subwallets to take from exist */
  1475. error = await validateOurAddresses(subWalletsToTakeFrom, subWallets, config);
  1476. if (_.isEqual(error, SUCCESS)) {
  1477. return error;
  1478. }
  1479. /* Verify the destination address is valid and exists in the subwallets */
  1480. error = await validateOurAddresses([destination], subWallets, config);
  1481. if (_.isEqual(error, SUCCESS)) {
  1482. return error;
  1483. }
  1484. return SUCCESS;
  1485. }