facade/SymbolFacade.js

/* eslint-disable no-unused-vars */
import { Bip32Node } from "../Bip32.js";
/* eslint-enable no-unused-vars */
import {
  Hash256,
  PrivateKey,
  PublicKey,
  /* eslint-disable no-unused-vars */
  SharedKey256,
  /* eslint-enable no-unused-vars */
  Signature,
} from "../CryptoTypes.js";
import { NetworkLocator } from "../Network.js";
import { KeyPair, Verifier } from "../symbol/KeyPair.js";
import { Address, Network as SymbolNetwork } from "../symbol/Network.js";
import { deriveSharedKey } from "../symbol/SharedKey.js";
import TransactionFactory from "../symbol/TransactionFactory.js";
import { MerkleHashBuilder } from "../symbol/merkle.js";
import * as sc from "../symbol/models.js";
import { sha3_256 } from "@noble/hashes/sha3";

const TRANSACTION_HEADER_SIZE = [
  4, // size
  4, // reserved1
  Signature.SIZE, // signature
  PublicKey.SIZE, // signer
  4, // reserved2
].reduce((x, y) => x + y);

const AGGREGATE_HASHED_SIZE = [
  4, // version, network, type
  8, // maxFee
  8, // deadline
  Hash256.SIZE, // transactionsHash
].reduce((x, y) => x + y);

const isAggregateTransaction = (transactionBuffer) => {
  const transactionTypeOffset = TRANSACTION_HEADER_SIZE + 2; // skip version and network byte
  const transactionType =
    (transactionBuffer[transactionTypeOffset + 1] << 8) +
    transactionBuffer[transactionTypeOffset];
  const aggregateTypes = [
    sc.TransactionType.AGGREGATE_BONDED.value,
    sc.TransactionType.AGGREGATE_COMPLETE.value,
  ];
  return aggregateTypes.some(
    (aggregateType) => aggregateType === transactionType
  );
};

const transactionDataBuffer = (transactionBuffer) => {
  const dataBufferStart = TRANSACTION_HEADER_SIZE;
  const dataBufferEnd = isAggregateTransaction(transactionBuffer)
    ? TRANSACTION_HEADER_SIZE + AGGREGATE_HASHED_SIZE
    : transactionBuffer.length;

  return transactionBuffer.subarray(dataBufferStart, dataBufferEnd);
};

/**
 * Facade used to interact with Symbol blockchain.
 */
export default class SymbolFacade {
  /**
   * BIP32 curve name.
   * @type {string}
   */
  static BIP32_CURVE_NAME = "ed25519";

  /**
   * Network address class type.
   * @type {Address}
   */
  static Address = Address;

  /**
   * Network key pair class type.
   * @type {KeyPair}
   */
  static KeyPair = KeyPair;

  /**
   * Network verifier class type.
   * @type {Verifier}
   */
  static Verifier = Verifier;

  /**
   * Derives shared key from key pair and other party's public key.
   * @param {KeyPair} keyPair Key pair.
   * @param {PublicKey} otherPublicKey Other party's public key.
   * @returns {SharedKey256} Shared encryption key.
   */
  static deriveSharedKey = deriveSharedKey;

  /**
   * Creates a Symbol facade.
   * @param {string|SymbolNetwork} network Symbol network or network name.
   */
  constructor(network) {
    /**
     * Underlying network.
     * @type SymbolNetwork
     */
    this.network =
      "string" === typeof network
        ? NetworkLocator.findByName(SymbolNetwork.NETWORKS, network)
        : network;

    /**
     * Underlying transaction factory.
     * @type TransactionFactory
     */
    this.transactionFactory = new TransactionFactory(this.network);
  }

  /**
   * Hashes a Symbol transaction.
   * @param {sc.Transaction} transaction Transaction object.
   * @returns {Hash256} Transaction hash.
   */
  hashTransaction(transaction) {
    const hasher = sha3_256.create();
    hasher.update(transaction.signature.bytes);
    hasher.update(transaction.signerPublicKey.bytes);
    hasher.update(this.network.generationHashSeed.bytes);
    hasher.update(transactionDataBuffer(transaction.serialize()));
    return new Hash256(hasher.digest());
  }

  /**
   * Signs a Symbol transaction.
   * @param {KeyPair} keyPair Key pair.
   * @param {sc.Transaction} transaction Transaction object.
   * @returns {Signature} Transaction signature.
   */
  signTransaction(keyPair, transaction) {
    return keyPair.sign(
      new Uint8Array([
        ...this.network.generationHashSeed.bytes,
        ...transactionDataBuffer(transaction.serialize()),
      ])
    );
  }

  /**
   * Verifies a Symbol transaction.
   * @param {sc.Transaction} transaction Transaction object.
   * @param {Signature} signature Signature to verify.
   * @returns {boolean} \c true if transaction signature is verified.
   */
  verifyTransaction(transaction, signature) {
    const verifyBuffer = new Uint8Array([
      ...this.network.generationHashSeed.bytes,
      ...transactionDataBuffer(transaction.serialize()),
    ]);
    return new Verifier(transaction.signerPublicKey).verify(
      verifyBuffer,
      signature
    );
  }

  /**
   * Cosigns a Symbol transaction.
   * @param {KeyPair} keyPair Key pair of the cosignatory.
   * @param {sc.Transaction} transaction Transaction object.
   * @param {boolean} detached \c true if resulting cosignature is appropriate for network propagation.
   *                           \c false if resulting cosignature is appropriate for attaching to an aggregate.
   * @returns {sc.Cosignature|sc.DetachedCosignature} Signed cosignature.
   */
  cosignTransaction(keyPair, transaction, detached = false) {
    const transactionHash = this.hashTransaction(transaction);

    const initializeCosignature = (cosignature) => {
      cosignature.version = 0n;
      cosignature.signerPublicKey = new sc.PublicKey(keyPair.publicKey.bytes);
      cosignature.signature = new sc.Signature(
        keyPair.sign(transactionHash.bytes).bytes
      );
    };

    if (detached) {
      const cosignature = new sc.DetachedCosignature();
      cosignature.parentHash = new sc.Hash256(transactionHash.bytes);
      initializeCosignature(cosignature);
      return cosignature;
    }

    const cosignature = new sc.Cosignature();
    initializeCosignature(cosignature);
    return cosignature;
  }

  /**
   * Hashes embedded transactions of an aggregate transaction.
   * @param {Array<sc.EmbeddedTransaction>} embeddedTransactions Embedded transactions to hash.
   * @returns {Hash256} Aggregate transactions hash.
   */
  static hashEmbeddedTransactions(embeddedTransactions) {
    const hashBuilder = new MerkleHashBuilder();
    embeddedTransactions.forEach((embeddedTransaction) => {
      hashBuilder.update(
        new Hash256(sha3_256(embeddedTransaction.serialize()))
      );
    });

    return hashBuilder.final();
  }

  /**
   * Creates a network compatible BIP32 path for the specified account.
   *
   * @param {number} accountId Id of the account for which to generate a BIP32 path.
   * @returns {Array<number>} BIP32 path for the specified account.
   */
  bip32Path(accountId) {
    return [44, "mainnet" === this.network.name ? 4343 : 1, accountId, 0, 0];
  }

  /**
   * Derives a Symbol KeyPair from a BIP32 node.
   * @param {Bip32Node} bip32Node BIP32 node.
   * @returns {KeyPair} Derived key pair.
   */
  static bip32NodeToKeyPair(bip32Node) {
    return new KeyPair(new PrivateKey(bip32Node.privateKey.bytes));
  }
}