From 818d3f048f7fc0aa41d558ecaf9db70eae34d7da Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Fri, 2 Jun 2023 17:31:41 +0200
Subject: [PATCH] fix(1442): Force insert for TXs, if no cautious mode (do not
 check is tx exists). - If cautious mode, insert TX using batch (delete then
 insert) - Migrate existing txs.db (add and fill missing columns)

---
 app/lib/dal/drivers/SQLiteDriver.ts           |   8 +-
 app/lib/dal/fileDAL.ts                        | 180 ++++++++++--------
 app/lib/dal/indexDAL/abstract/TxsDAO.ts       |  10 +-
 .../dal/indexDAL/sqlite/SqliteTransactions.ts |  33 ++--
 app/lib/dal/sqliteDAL/MetaDAL.ts              |   8 +-
 app/lib/db/DBTx.ts                            |  17 +-
 app/lib/dto/TransactionDTO.ts                 |  88 ++++-----
 7 files changed, 188 insertions(+), 156 deletions(-)

diff --git a/app/lib/dal/drivers/SQLiteDriver.ts b/app/lib/dal/drivers/SQLiteDriver.ts
index 2fe0faf78..d034f0a03 100644
--- a/app/lib/dal/drivers/SQLiteDriver.ts
+++ b/app/lib/dal/drivers/SQLiteDriver.ts
@@ -95,12 +95,12 @@ export class SQLiteDriver {
   }
 
   async destroyDatabase(): Promise<void> {
-    this.logger.debug("Removing SQLite database...");
+    this.logger.debug("Removing SQLite database \"%s\"...", this.path);
     await this.closeConnection();
     if (this.path !== MEMORY_PATH) {
       await RealFS().fsUnlink(this.path);
     }
-    this.logger.debug("Database removed");
+    this.logger.debug("Database \"%s\" removed", this.path);
   }
 
   get closed() {
@@ -116,9 +116,9 @@ export class SQLiteDriver {
       db.open; // For an unknown reason, we need this line.
     }
     await new Promise((resolve, reject) => {
-      this.logger.debug("Trying to close SQLite...");
+      this.logger.debug("Closing SQLite database \"%s\"...", this.path);
       db.on("close", () => {
-        this.logger.info("Database closed.");
+        this.logger.info("Database \"%s\" closed.", this.path);
         this.dbPromise = null;
         resolve();
       });
diff --git a/app/lib/dal/fileDAL.ts b/app/lib/dal/fileDAL.ts
index 53ca48384..100d1cb51 100644
--- a/app/lib/dal/fileDAL.ts
+++ b/app/lib/dal/fileDAL.ts
@@ -13,12 +13,12 @@
 
 import * as fs from "fs";
 import * as path from "path";
-import { SQLiteDriver } from "./drivers/SQLiteDriver";
-import { ConfDAL } from "./fileDALs/ConfDAL";
-import { ConfDTO } from "../dto/ConfDTO";
-import { BlockDTO } from "../dto/BlockDTO";
-import { DBHead } from "../db/DBHead";
-import { DBIdentity, IdentityDAL } from "./sqliteDAL/IdentityDAL";
+import {SQLiteDriver} from "./drivers/SQLiteDriver";
+import {ConfDAL} from "./fileDALs/ConfDAL";
+import {ConfDTO} from "../dto/ConfDTO";
+import {BlockDTO} from "../dto/BlockDTO";
+import {DBHead} from "../db/DBHead";
+import {DBIdentity, IdentityDAL} from "./sqliteDAL/IdentityDAL";
 import {
   CindexEntry,
   FullCindexEntry,
@@ -31,56 +31,55 @@ import {
   SimpleUdEntryForWallet,
   SindexEntry,
 } from "../indexer";
-import { TransactionDTO } from "../dto/TransactionDTO";
-import { CertDAL, DBCert } from "./sqliteDAL/CertDAL";
-import { DBBlock } from "../db/DBBlock";
-import { DBMembership, MembershipDAL } from "./sqliteDAL/MembershipDAL";
-import { MerkleDTO } from "../dto/MerkleDTO";
-import { CommonConstants } from "../common-libs/constants";
-import { PowDAL } from "./fileDALs/PowDAL";
-import { Initiable } from "./sqliteDAL/Initiable";
-import { MetaDAL } from "./sqliteDAL/MetaDAL";
-import { DataErrors } from "../common-libs/errors";
-import { BasicRevocableIdentity, IdentityDTO } from "../dto/IdentityDTO";
-import { FileSystem } from "../system/directory";
-import { Wot } from "../../../neon/lib";
-import { IIndexDAO } from "./indexDAL/abstract/IIndexDAO";
-import { BIndexDAO } from "./indexDAL/abstract/BIndexDAO";
-import { MIndexDAO } from "./indexDAL/abstract/MIndexDAO";
-import { SIndexDAO } from "./indexDAL/abstract/SIndexDAO";
-import { CIndexDAO } from "./indexDAL/abstract/CIndexDAO";
-import { IdentityForRequirements } from "../../service/BlockchainService";
-import { NewLogger } from "../logger";
-import { BlockchainDAO } from "./indexDAL/abstract/BlockchainDAO";
-import { TxsDAO } from "./indexDAL/abstract/TxsDAO";
-import { WalletDAO } from "./indexDAL/abstract/WalletDAO";
-import { PeerDAO } from "./indexDAL/abstract/PeerDAO";
-import { DBTx } from "../db/DBTx";
-import { DBWallet } from "../db/DBWallet";
-import { Tristamp } from "../common/Tristamp";
-import { CFSCore } from "./fileDALs/CFSCore";
-import { Underscore } from "../common-libs/underscore";
-import { DBPeer } from "../db/DBPeer";
-import { MonitorFlushedIndex } from "../debug/MonitorFlushedIndex";
-import { cliprogram } from "../common-libs/programOptions";
-import { DividendDAO, UDSource } from "./indexDAL/abstract/DividendDAO";
-import { HttpSource, HttpUD } from "../../modules/bma/lib/dtos";
-import { GenericDAO } from "./indexDAL/abstract/GenericDAO";
-import { MonitorExecutionTime } from "../debug/MonitorExecutionTime";
-import { LevelDBDividend } from "./indexDAL/leveldb/LevelDBDividend";
-import { LevelDBBindex } from "./indexDAL/leveldb/LevelDBBindex";
-
-import { LevelUp } from "levelup";
-import { LevelDBBlockchain } from "./indexDAL/leveldb/LevelDBBlockchain";
-import { LevelDBSindex } from "./indexDAL/leveldb/LevelDBSindex";
-import { SqliteTransactions } from "./indexDAL/sqlite/SqliteTransactions";
-import { SqlitePeers } from "./indexDAL/sqlite/SqlitePeers";
-import { LevelDBWallet } from "./indexDAL/leveldb/LevelDBWallet";
-import { LevelDBCindex } from "./indexDAL/leveldb/LevelDBCindex";
-import { LevelDBIindex } from "./indexDAL/leveldb/LevelDBIindex";
-import { LevelDBMindex } from "./indexDAL/leveldb/LevelDBMindex";
-import { ConfDAO } from "./indexDAL/abstract/ConfDAO";
-import { ServerDAO } from "./server-dao";
+import {TransactionDTO} from "../dto/TransactionDTO";
+import {CertDAL, DBCert} from "./sqliteDAL/CertDAL";
+import {DBBlock} from "../db/DBBlock";
+import {DBMembership, MembershipDAL} from "./sqliteDAL/MembershipDAL";
+import {MerkleDTO} from "../dto/MerkleDTO";
+import {CommonConstants} from "../common-libs/constants";
+import {PowDAL} from "./fileDALs/PowDAL";
+import {Initiable} from "./sqliteDAL/Initiable";
+import {MetaDAL} from "./sqliteDAL/MetaDAL";
+import {DataErrors} from "../common-libs/errors";
+import {BasicRevocableIdentity, IdentityDTO} from "../dto/IdentityDTO";
+import {FileSystem} from "../system/directory";
+import {Wot} from "../../../neon/lib";
+import {IIndexDAO} from "./indexDAL/abstract/IIndexDAO";
+import {BIndexDAO} from "./indexDAL/abstract/BIndexDAO";
+import {MIndexDAO} from "./indexDAL/abstract/MIndexDAO";
+import {SIndexDAO} from "./indexDAL/abstract/SIndexDAO";
+import {CIndexDAO} from "./indexDAL/abstract/CIndexDAO";
+import {IdentityForRequirements} from "../../service/BlockchainService";
+import {BlockchainDAO} from "./indexDAL/abstract/BlockchainDAO";
+import {TxsDAO} from "./indexDAL/abstract/TxsDAO";
+import {WalletDAO} from "./indexDAL/abstract/WalletDAO";
+import {PeerDAO} from "./indexDAL/abstract/PeerDAO";
+import {DBTx} from "../db/DBTx";
+import {DBWallet} from "../db/DBWallet";
+import {Tristamp} from "../common/Tristamp";
+import {CFSCore} from "./fileDALs/CFSCore";
+import {Underscore} from "../common-libs/underscore";
+import {DBPeer} from "../db/DBPeer";
+import {MonitorFlushedIndex} from "../debug/MonitorFlushedIndex";
+import {cliprogram} from "../common-libs/programOptions";
+import {DividendDAO, UDSource} from "./indexDAL/abstract/DividendDAO";
+import {HttpSource, HttpUD} from "../../modules/bma/lib/dtos";
+import {GenericDAO} from "./indexDAL/abstract/GenericDAO";
+import {MonitorExecutionTime} from "../debug/MonitorExecutionTime";
+import {LevelDBDividend} from "./indexDAL/leveldb/LevelDBDividend";
+import {LevelDBBindex} from "./indexDAL/leveldb/LevelDBBindex";
+
+import {LevelUp} from "levelup";
+import {LevelDBBlockchain} from "./indexDAL/leveldb/LevelDBBlockchain";
+import {LevelDBSindex} from "./indexDAL/leveldb/LevelDBSindex";
+import {SqliteTransactions} from "./indexDAL/sqlite/SqliteTransactions";
+import {SqlitePeers} from "./indexDAL/sqlite/SqlitePeers";
+import {LevelDBWallet} from "./indexDAL/leveldb/LevelDBWallet";
+import {LevelDBCindex} from "./indexDAL/leveldb/LevelDBCindex";
+import {LevelDBIindex} from "./indexDAL/leveldb/LevelDBIindex";
+import {LevelDBMindex} from "./indexDAL/leveldb/LevelDBMindex";
+import {ConfDAO} from "./indexDAL/abstract/ConfDAO";
+import {ServerDAO} from "./server-dao";
 
 const readline = require("readline");
 const indexer = require("../indexer").Indexer;
@@ -830,7 +829,7 @@ export class FileDAL implements ServerDAO {
   }
 
   removeTxByHash(hash: string) {
-    return this.txsDAL.removeTX(hash);
+    return this.txsDAL.removeByHash(hash);
   }
 
   getTransactionsPending(versionMin = 0) {
@@ -1304,11 +1303,11 @@ export class FileDAL implements ServerDAO {
       const from = await this.getWrittenIdtyByPubkeyForWotbID(entry.issuer);
       const to = await this.getWrittenIdtyByPubkeyForWotbID(entry.receiver);
       if (entry.op == CommonConstants.IDX_CREATE) {
-        // NewLogger().trace('addLink %s -> %s', from.wotb_id, to.wotb_id)
+        // logger.trace('addLink %s -> %s', from.wotb_id, to.wotb_id)
         wotb.addLink(from.wotb_id, to.wotb_id);
       } else {
         // Update = removal
-        NewLogger().trace("removeLink %s -> %s", from.wotb_id, to.wotb_id);
+        //logger.trace("removeLink %s -> %s", from.wotb_id, to.wotb_id);
         wotb.removeLink(from.wotb_id, to.wotb_id);
       }
     }
@@ -1350,28 +1349,57 @@ export class FileDAL implements ServerDAO {
     return this.writeSideFileOfBlock(block);
   }
 
+  /**
+   * Map tx DTO into DBtxs
+   * @param txs
+   * @param block_number
+   * @param medianTime
+   * @private
+   */
+  private async mapToDBTxs(
+      txs: TransactionDTO[],
+      block_number: number,
+      medianTime: number
+  ): Promise<DBTx[]> {
+    return Promise.all(
+        txs.map(async (tx) => {
+          const sp = tx.blockstamp.split("-", 2);
+          const basedBlock = (await this.getAbsoluteBlockByNumberAndHash(
+              parseInt(sp[0]),
+              sp[1]
+          )) as DBBlock;
+          tx.blockstampTime = basedBlock.medianTime;
+          const txEntity = TransactionDTO.fromJSONObject(tx);
+          if (!txEntity.hash) txEntity.computeAllHashes();
+          const dbTx = DBTx.fromTransactionDTO(txEntity);
+          dbTx.written = true;
+          dbTx.block_number = block_number;
+          dbTx.time = medianTime;
+          return dbTx;
+        })
+    );
+  }
+
   async saveTxsInFiles(
     txs: TransactionDTO[],
     block_number: number,
     medianTime: number
   ) {
-    return Promise.all(
-      txs.map(async (tx) => {
-        const sp = tx.blockstamp.split("-");
-        const basedBlock = (await this.getAbsoluteBlockByNumberAndHash(
-          parseInt(sp[0]),
-          sp[1]
-        )) as DBBlock;
-        tx.blockstampTime = basedBlock.medianTime;
-        const txEntity = TransactionDTO.fromJSONObject(tx);
-        txEntity.computeAllHashes();
-        return this.txsDAL.addLinked(
-          TransactionDTO.fromJSONObject(txEntity),
-          block_number,
-          medianTime
-        );
-      })
-    );
+    if (!txs.length) return [];
+    const records = await this.mapToDBTxs(txs, block_number, medianTime);
+    await this.txsDAL.saveBatch(records);
+    return records;
+  }
+
+  async insertTxsInFiles(
+      txs: TransactionDTO[],
+      block_number: number,
+      medianTime: number
+  ): Promise<DBTx[]> {
+    if (!txs.length) return [];
+    const dbTxs = await this.mapToDBTxs(txs, block_number, medianTime);
+    await this.txsDAL.insertBatch(dbTxs);
+    return dbTxs;
   }
 
   async merkleForPeers() {
diff --git a/app/lib/dal/indexDAL/abstract/TxsDAO.ts b/app/lib/dal/indexDAL/abstract/TxsDAO.ts
index 7ef9e06de..93854de3e 100644
--- a/app/lib/dal/indexDAL/abstract/TxsDAO.ts
+++ b/app/lib/dal/indexDAL/abstract/TxsDAO.ts
@@ -10,6 +10,12 @@ export interface TxsDAO extends GenericDAO<DBTx> {
 
   getTX(hash: string): Promise<DBTx>;
 
+  /**
+   * Make a batch insert or update.
+   * @param records The records to insert or update as a batch.
+   */
+  saveBatch(records: DBTx[]): Promise<void>;
+
   addLinked(
     tx: TransactionDTO,
     block_number: number,
@@ -51,7 +57,9 @@ export interface TxsDAO extends GenericDAO<DBTx> {
 
   getPendingWithRecipient(pubkey: string): Promise<DBTx[]>;
 
-  removeTX(hash: string): Promise<void>;
+  removeByHash(hash: string): Promise<void>;
+
+  removeByHashBatch(hashArray: string[]): Promise<void>;
 
   removeAll(): Promise<void>;
 
diff --git a/app/lib/dal/indexDAL/sqlite/SqliteTransactions.ts b/app/lib/dal/indexDAL/sqlite/SqliteTransactions.ts
index 28f36b9ae..b8846d704 100644
--- a/app/lib/dal/indexDAL/sqlite/SqliteTransactions.ts
+++ b/app/lib/dal/indexDAL/sqlite/SqliteTransactions.ts
@@ -79,24 +79,23 @@ export class SqliteTransactions extends SqliteTable<DBTx> implements TxsDAO {
    */
 
   @MonitorExecutionTime()
-  async insert(record: DBTx): Promise<void> {
-    this.onBeforeInsert(record);
-    await this.insertInTable(this.driver, record);
+  insert(record: DBTx): Promise<void> {
+    return this.insertInTable(this.driver, record);
   }
 
   @MonitorExecutionTime()
   async insertBatch(records: DBTx[]): Promise<void> {
     if (records.length) {
-      records.forEach(r => this.onBeforeInsert(r));
       return this.insertBatchInTable(this.driver, records);
     }
   }
 
-  onBeforeInsert(dbTx: DBTx) {
-    // Compute unique issuer/recipient (need to improve tx history)
-    dbTx.issuer = (dbTx.issuers.length === 1) ? dbTx.issuers[0] : null;
-    const recipients = !dbTx.issuer ? dbTx.recipients : dbTx.recipients.filter(r => r !== dbTx.issuer);
-    dbTx.recipient = (recipients.length === 1) ? recipients[0] : null;
+  @MonitorExecutionTime()
+  async saveBatch(records: DBTx[]): Promise<void> {
+    if (records.length) {
+      await this.removeByHashBatch(records.map(t => t.hash));
+      await this.insertBatch(records);
+    }
   }
 
   sandbox: SandBox<{
@@ -184,8 +183,8 @@ export class SqliteTransactions extends SqliteTable<DBTx> implements TxsDAO {
     to: number
   ): Promise<{ sent: DBTx[]; received: DBTx[] }> {
     return {
-      sent: await this.getLinkedWithIssuerByRange('blockstampTime', pubkey, from, to),
-      received: await this.getLinkedWithRecipientByRange('blockstampTime', pubkey, from, to)
+      sent: await this.getLinkedWithIssuerByRange('time', pubkey, from, to),
+      received: await this.getLinkedWithRecipientByRange('time', pubkey, from, to)
     };
   }
 
@@ -294,7 +293,17 @@ export class SqliteTransactions extends SqliteTable<DBTx> implements TxsDAO {
     );
   }
 
-  removeTX(hash: string): Promise<void> {
+  async removeByHashBatch(hashArray: string[]): Promise<void> {
+    let i = 0;
+    // Delete by slice of 500 items (because SQLite IN operator is limited)
+    while (i < hashArray.length - 1) {
+      const slice = hashArray.slice(i, i + 500);
+      await this.driver.sqlWrite(`DELETE FROM txs WHERE hash IN (${slice.map(_ => '?')})`, slice);
+      i += 500;
+    }
+  }
+
+  removeByHash(hash: string): Promise<void> {
     return this.driver.sqlWrite("DELETE FROM txs WHERE hash = ?", [hash]);
   }
 
diff --git a/app/lib/dal/sqliteDAL/MetaDAL.ts b/app/lib/dal/sqliteDAL/MetaDAL.ts
index b74e6dd82..4f4672c8d 100644
--- a/app/lib/dal/sqliteDAL/MetaDAL.ts
+++ b/app/lib/dal/sqliteDAL/MetaDAL.ts
@@ -192,14 +192,15 @@ export class MetaDAL extends AbstractSQLite<DBMeta> {
     // Wrong transaction storage
     25: async () => {},
 
-    // Add columns 'issuer' and 'recipient' in transaction table - see issue #1442
+    // Drop old table 'txs' (replaced by a file 'txs.db')
     26: async() => {
-      // Drop old table 'txs' (replaced by a file 'txs.db')
       await this.exec("BEGIN;" +
           "DROP TABLE IF EXISTS txs;" +
           "COMMIT;")
+    },
 
-      // Migrate txs.db
+    // Add columns 'issuer' and 'recipient' in transaction table - see issue #1442
+    27: async() => {
       const txsDriver = await this.getSqliteDB("txs.db");
       const txsDAL = new MetaDAL(txsDriver, this.getSqliteDB);
 
@@ -214,6 +215,7 @@ export class MetaDAL extends AbstractSQLite<DBMeta> {
           "DROP INDEX IF EXISTS idx_txs_received;" +
           "DROP INDEX IF EXISTS idx_txs_output_base;" +
           "DROP INDEX IF EXISTS idx_txs_output_amount;" +
+          "CREATE INDEX IF NOT EXISTS idx_txs_issuers ON txs (issuers);" +
           "CREATE INDEX IF NOT EXISTS idx_txs_recipients ON txs (recipients);" +
           "COMMIT;"
       );
diff --git a/app/lib/db/DBTx.ts b/app/lib/db/DBTx.ts
index 4b74b8457..bcb1954de 100644
--- a/app/lib/db/DBTx.ts
+++ b/app/lib/db/DBTx.ts
@@ -46,18 +46,13 @@ export class DBTx {
     dbTx.removed = false;
     dbTx.output_base = tx.output_base;
     dbTx.output_amount = tx.output_amount;
-    return dbTx;
-  }
 
-  static setRecipients(txs: DBTx[]) {
-    // Each transaction must have a good "recipients" field for future searchs
-    txs.forEach((tx) => (tx.recipients = DBTx.outputs2recipients(tx)));
-  }
+    // Computed columns (unique issuer and/or recipient)
+    dbTx.issuer = (dbTx.issuers.length === 1) ? dbTx.issuers[0] : null;
+    const recipients = !dbTx.issuer ? dbTx.recipients : dbTx.recipients.filter(r => r !== dbTx.issuer);
+    dbTx.recipient = (recipients.length === 1) ? recipients[0] : null;
 
-  static outputs2recipients(tx: DBTx) {
-    return tx.outputs.map(function (out) {
-      const recipent = out.match("SIG\\((.*)\\)");
-      return (recipent && recipent[1]) || "UNKNOWN";
-    });
+    return dbTx;
   }
+
 }
diff --git a/app/lib/dto/TransactionDTO.ts b/app/lib/dto/TransactionDTO.ts
index 71dfa660f..618488046 100644
--- a/app/lib/dto/TransactionDTO.ts
+++ b/app/lib/dto/TransactionDTO.ts
@@ -14,6 +14,7 @@
 import { hashf } from "../common";
 import { Cloneable } from "./Cloneable";
 import { verify } from "../../../neon/lib";
+import {CommonConstants} from "../common-libs/constants";
 
 export interface BaseDTO {
   base: number;
@@ -74,8 +75,8 @@ export class TransactionDTO implements Cloneable {
     public currency: string,
     public locktime: number,
     public hash: string,
-    public blockstamp: string,
-    public blockstampTime: number,
+    public blockstamp: string, // Reference block of the TX
+    public blockstampTime: number, // Median time of the reference block
     public issuers: string[],
     public inputs: string[],
     public outputs: string[],
@@ -95,14 +96,14 @@ export class TransactionDTO implements Cloneable {
 
   get output_amount() {
     return this.outputs.reduce(
-      (maxBase, output) => Math.max(maxBase, parseInt(output.split(":")[0])),
+      (sum, output) => sum + parseInt(output.split(":")[0]),
       0
     );
   }
 
   get output_base() {
     return this.outputs.reduce(
-      (sum, output) => sum + parseInt(output.split(":")[1]),
+      (maxBase, output) => Math.max(maxBase, parseInt(output.split(":")[1])),
       0
     );
   }
@@ -126,8 +127,11 @@ export class TransactionDTO implements Cloneable {
   }
 
   getHash() {
-    const raw = TransactionDTO.toRAW(this);
-    return hashf(raw);
+    if (!this.hash) {
+      const raw = TransactionDTO.toRAW(this);
+      this.hash = hashf(raw).toUpperCase();
+    }
+    return this.hash;
   }
 
   getRawTxNoSig() {
@@ -161,40 +165,26 @@ export class TransactionDTO implements Cloneable {
   }
 
   outputsAsRecipients(): string[] {
-    return this.outputs.map((out) => {
-      const recipent = out.match("SIG\\((.*)\\)");
-      return (recipent && recipent[1]) || "UNKNOWN";
-    });
+    return this.outputs.reduce((res, output) => {
+      let match: any;
+      const recipients: string[] = [];
+      while (output && (match = CommonConstants.TRANSACTION.OUTPUT_CONDITION_SIG_PUBKEY.exec(output)) !== null) {
+        const pub = match[1] as string;
+        if (!res.includes(pub) && !recipients.includes(pub)) {
+          recipients.push(pub)
+        }
+        output = output.substring(match.index + match[0].length);
+      }
+      if (recipients.length) {
+        return res.concat(recipients);
+      }
+      if (res.includes("UNKNOWN")) return res;
+      return res.concat("UNKNOWN");
+    }, <string[]>[]);
   }
 
   getRaw() {
-    let raw = "";
-    raw += "Version: " + this.version + "\n";
-    raw += "Type: Transaction\n";
-    raw += "Currency: " + this.currency + "\n";
-    raw += "Blockstamp: " + this.blockstamp + "\n";
-    raw += "Locktime: " + this.locktime + "\n";
-    raw += "Issuers:\n";
-    (this.issuers || []).forEach((issuer) => {
-      raw += issuer + "\n";
-    });
-    raw += "Inputs:\n";
-    this.inputs.forEach((input) => {
-      raw += input + "\n";
-    });
-    raw += "Unlocks:\n";
-    this.unlocks.forEach((unlock) => {
-      raw += unlock + "\n";
-    });
-    raw += "Outputs:\n";
-    this.outputs.forEach((output) => {
-      raw += output + "\n";
-    });
-    raw += "Comment: " + (this.comment || "") + "\n";
-    this.signatures.forEach((signature) => {
-      raw += signature + "\n";
-    });
-    return raw;
+    return TransactionDTO.toRAW(this);
   }
 
   getCompactVersion() {
@@ -231,7 +221,7 @@ export class TransactionDTO implements Cloneable {
   }
 
   computeAllHashes() {
-    this.hash = hashf(this.getRaw()).toUpperCase();
+    this.hash = this.getHash();
   }
 
   json() {
@@ -293,32 +283,32 @@ export class TransactionDTO implements Cloneable {
     );
   }
 
-  static toRAW(json: TransactionDTO, noSig = false) {
+  static toRAW(tx: TransactionDTO, noSig = false) {
     let raw = "";
-    raw += "Version: " + json.version + "\n";
+    raw += "Version: " + tx.version + "\n";
     raw += "Type: Transaction\n";
-    raw += "Currency: " + json.currency + "\n";
-    raw += "Blockstamp: " + json.blockstamp + "\n";
-    raw += "Locktime: " + json.locktime + "\n";
+    raw += "Currency: " + tx.currency + "\n";
+    raw += "Blockstamp: " + tx.blockstamp + "\n";
+    raw += "Locktime: " + tx.locktime + "\n";
     raw += "Issuers:\n";
-    (json.issuers || []).forEach((issuer) => {
+    (tx.issuers || []).forEach((issuer) => {
       raw += issuer + "\n";
     });
     raw += "Inputs:\n";
-    (json.inputs || []).forEach((input) => {
+    (tx.inputs || []).forEach((input) => {
       raw += input + "\n";
     });
     raw += "Unlocks:\n";
-    (json.unlocks || []).forEach((unlock) => {
+    (tx.unlocks || []).forEach((unlock) => {
       raw += unlock + "\n";
     });
     raw += "Outputs:\n";
-    (json.outputs || []).forEach((output) => {
+    (tx.outputs || []).forEach((output) => {
       raw += output + "\n";
     });
-    raw += "Comment: " + (json.comment || "") + "\n";
+    raw += "Comment: " + (tx.comment || "") + "\n";
     if (!noSig) {
-      (json.signatures || []).forEach((signature) => {
+      (tx.signatures || []).forEach((signature) => {
         raw += signature + "\n";
       });
     }
-- 
GitLab