PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-coin-icp/src/lib

Просмотр файла: utils.ts

import {
  BaseUtils,
  KeyPair,
  ParseTransactionError,
  Recipient,
  BuildTransactionError,
  MethodNotImplementedError,
} from '@bitgo/sdk-core';
import { Principal as DfinityPrincipal } from '@dfinity/principal';
import * as agent from '@dfinity/agent';
import crypto from 'crypto';
import crc32 from 'crc-32';
import {
  HttpCanisterUpdate,
  IcpTransactionData,
  RequestType,
  Signatures,
  MetaData,
  SendArgs,
  PayloadsData,
  CurveType,
  AccountIdentifierHash,
  CborUnsignedTransaction,
  MAX_INGRESS_TTL,
} from './iface';
import { KeyPair as IcpKeyPair } from './keyPair';
const messageCompiled = require('../../resources/messageCompiled');
const { encode, decode, Encoder } = require('cbor-x/index-no-eval'); // The "cbor-x" library is used here because it supports modern features like BigInt. do not replace it with "cbor as "cbor" is not compatible with Rust's serde_cbor when handling big numbers.
import js_sha256 from 'js-sha256';
import BigNumber from 'bignumber.js';
import { secp256k1 } from '@noble/curves/secp256k1';

//custom encoder that avoids tagging
const encoder = new Encoder({
  structuredClone: false,
  useToJSON: false,
  mapsAsObjects: false,
  largeBigIntToFloat: false,
});

export class Utils implements BaseUtils {
  /** @inheritdoc */
  isValidSignature(signature: string): boolean {
    throw new MethodNotImplementedError();
  }

  /**
   * gets the gas data of this transaction.
   */
  //TODO WIN-4242: to moved to a config and eventually to an API for dynamic value
  feeData(): string {
    return '-10000';
  }

  /**
   * Checks if the provided address is a valid ICP address.
   *
   * @param {string} address - The address to validate.
   * @returns {boolean} - Returns `true` if the address is valid, otherwise `false`.
   */
  isValidAddress(address: string): boolean {
    const rootAddress = this.validateMemoAndReturnRootAddress(address);
    return rootAddress !== undefined && this.isValidHash(rootAddress);
  }

  /**
   * Validates the memo ID in the address and returns the root address.
   *
   * @param {string} address - The address to validate and extract the root address from.
   * @returns {string | undefined} - The root address if valid, otherwise `undefined`.
   */
  validateMemoAndReturnRootAddress(address: string): string | undefined {
    if (!address) {
      return undefined;
    }
    const [rootAddress, memoId] = address.split('?memoId=');
    if (memoId && this.validateMemo(BigInt(memoId))) {
      return rootAddress;
    }
    return address;
  }

  /**
   * Checks if the provided hex string is a valid public key.
   *
   * A valid public key can be either compressed or uncompressed:
   * - Compressed public keys are 33 bytes long and start with either 0x02 or 0x03.
   * - Uncompressed public keys are 65 bytes long and start with 0x04.
   *
   * @param {string} hexStr - The hex string representation of the public key to validate.
   * @returns {boolean} - Returns `true` if the hex string is a valid public key, otherwise `false`.
   */
  isValidPublicKey(hexStr: string): boolean {
    if (!this.isValidHex(hexStr) || !this.isValidLength(hexStr)) {
      return false;
    }

    const pubKeyBytes = this.hexToBytes(hexStr);
    const firstByte = pubKeyBytes[0];
    const validCompressed = pubKeyBytes.length === 33 && (firstByte === 2 || firstByte === 3);
    const validUncompressed = pubKeyBytes.length === 65 && firstByte === 4;

    return validCompressed || validUncompressed;
  }

  /**
   * Encodes a value into CBOR format and returns it as a hex string.
   *
   * @param {unknown} value - The value to encode.
   * @returns {string} - The CBOR encoded value as a hex string.
   */
  cborEncode(value: unknown): string {
    if (value === undefined) {
      throw new Error('Value to encode cannot be undefined.');
    }
    const cborData = encode(value);
    return Buffer.from(cborData).toString('hex');
  }

  /**
   * Checks if the length of the given hexadecimal string is valid.
   * A valid length is either 66 characters (33 bytes) or 130 characters (65 bytes).
   *
   * @param {string} hexStr - The hexadecimal string to check.
   * @returns {boolean} - Returns `true` if the length is valid, otherwise `false`.
   */
  isValidLength(hexStr: string): boolean {
    return hexStr.length / 2 === 33 || hexStr.length / 2 === 65;
  }

  /**
   * Checks if the provided string is a valid hexadecimal string.
   *
   * A valid hexadecimal string consists of pairs of hexadecimal digits (0-9, a-f, A-F).
   *
   * @param hexStr - The string to be validated as a hexadecimal string.
   * @returns True if the string is a valid hexadecimal string, false otherwise.
   */
  isValidHex(hexStr: string): boolean {
    return /^([0-9a-fA-F]{2})+$/.test(hexStr);
  }

  /**
   * Converts a hexadecimal string to a Uint8Array.
   *
   * @param {string} hex - The hexadecimal string to convert.
   * @returns {Uint8Array} The resulting byte array.
   */
  hexToBytes(hex: string): Uint8Array {
    const bytes = new Uint8Array(hex.length / 2);
    for (let i = 0; i < hex.length; i += 2) {
      bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
    }
    return bytes;
  }

  /** @inheritdoc */
  isValidPrivateKey(key: string): boolean {
    return this.isValidKey(key);
  }

  /**
   * Validates whether the provided key is a valid ICP private key.
   *
   * This function attempts to create a new instance of `IcpKeyPair` using the provided key.
   * If the key is valid, the function returns `true`. If the key is invalid, an error is thrown,
   * and the function returns `false`.
   *
   * @param {string} key - The private key to validate.
   * @returns {boolean} - `true` if the key is valid, `false` otherwise.
   */
  isValidKey(key: string): boolean {
    try {
      new IcpKeyPair({ prv: key });
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Compresses an uncompressed public key.
   *
   * @param {string} uncompressedKey - The uncompressed public key in hexadecimal format.
   * @returns {string} - The compressed public key in hexadecimal format.
   * @throws {Error} - If the input key is not a valid uncompressed public key.
   */
  compressPublicKey(uncompressedKey: string): string {
    if (uncompressedKey.startsWith('02') || uncompressedKey.startsWith('03')) {
      return uncompressedKey;
    }
    if (!uncompressedKey.startsWith('04') || uncompressedKey.length !== 130) {
      throw new Error('Invalid uncompressed public key format.');
    }

    const xHex = uncompressedKey.slice(2, 66);
    const yHex = uncompressedKey.slice(66);
    const y = BigInt(`0x${yHex}`);
    const prefix = y % 2n === 0n ? '02' : '03';

    return `${prefix}${xHex}`;
  }

  /**
   * Converts a public key from its hexadecimal string representation to DER format.
   *
   * @param {string} publicKeyHex - The public key in hexadecimal string format.
   * @returns The public key in DER format as a Uint8Array.
   */
  getPublicKeyInDERFormat(publicKeyHex: string): Uint8Array {
    const publicKeyBuffer = Buffer.from(publicKeyHex, 'hex');
    const ellipticKey = secp256k1.ProjectivePoint.fromHex(publicKeyBuffer.toString('hex'));
    const uncompressedPublicKeyHex = ellipticKey.toHex(false);
    const derEncodedKey = agent.wrapDER(Buffer.from(uncompressedPublicKeyHex, 'hex'), agent.SECP256K1_OID);
    return derEncodedKey;
  }

  /**
   * Converts a public key in hexadecimal format to a Dfinity Principal ID.
   *
   * @param {string} publicKeyHex - The public key in hexadecimal format.
   * @returns The corresponding Dfinity Principal ID.
   */
  getPrincipalIdFromPublicKey(publicKeyHex: string): DfinityPrincipal {
    const derEncodedKey = this.getPublicKeyInDERFormat(publicKeyHex);
    const principalId = DfinityPrincipal.selfAuthenticating(Buffer.from(derEncodedKey));
    return principalId;
  }

  /**
   * Derives a DfinityPrincipal from a given public key in hexadecimal format.
   *
   * @param {string} publicKeyHex - The public key in hexadecimal format.
   * @returns The derived DfinityPrincipal.
   * @throws Will throw an error if the principal cannot be derived from the public key.
   */
  derivePrincipalFromPublicKey(publicKeyHex: string): DfinityPrincipal {
    try {
      const derEncodedKey = this.getPublicKeyInDERFormat(publicKeyHex);
      const principalId = DfinityPrincipal.selfAuthenticating(Buffer.from(derEncodedKey));
      const principal = DfinityPrincipal.fromUint8Array(principalId.toUint8Array());
      return principal;
    } catch (error) {
      throw new Error(`Failed to derive principal from public key: ${error.message}`);
    }
  }

  /**
   * Converts a DfinityPrincipal and an optional subAccount to a string representation of an account ID.
   *
   * @param {DfinityPrincipal} principal - The principal to convert.
   * @param {Uint8Array} [subAccount=new Uint8Array(32)] - An optional sub-account, defaults to a 32-byte array of zeros.
   * @returns {string} The hexadecimal string representation of the account ID.
   */
  fromPrincipal(principal: DfinityPrincipal, subAccount: Uint8Array = new Uint8Array(32)): string {
    const principalBytes = Buffer.from(principal.toUint8Array().buffer);
    return this.getAccountIdFromPrincipalBytes(this.getAccountIdPrefix(), principalBytes, subAccount);
  }

  getAccountIdFromPrincipalBytes(
    ACCOUNT_ID_PREFIX: Buffer<ArrayBuffer>,
    principalBytes: Buffer<ArrayBufferLike>,
    subAccount: Uint8Array<ArrayBufferLike>
  ): string {
    const combinedBytes = Buffer.concat([ACCOUNT_ID_PREFIX, principalBytes, subAccount]);
    const sha224Hash = crypto.createHash('sha224').update(combinedBytes).digest();
    const checksum = Buffer.alloc(4);
    checksum.writeUInt32BE(crc32.buf(sha224Hash) >>> 0, 0);
    const accountIdBytes = Buffer.concat([checksum, sha224Hash]);
    return accountIdBytes.toString('hex');
  }

  /**
   * Retrieves the address associated with a given hex-encoded public key.
   *
   * @param {string} hexEncodedPublicKey - The public key in hex-encoded format.
   * @returns {Promise<string>} A promise that resolves to the address derived from the provided public key.
   * @throws {Error} Throws an error if the provided public key is not in a valid hex-encoded format.
   */
  async getAddressFromPublicKey(hexEncodedPublicKey: string): Promise<string> {
    if (!this.isValidPublicKey(hexEncodedPublicKey)) {
      throw new Error('Invalid hex-encoded public key format.');
    }
    const compressedKey = this.compressPublicKey(hexEncodedPublicKey);
    const keyPair = new IcpKeyPair({ pub: compressedKey });
    return keyPair.getAddress();
  }

  /**
   * Generates a new key pair. If a seed is provided, it will be used to generate the key pair.
   *
   * @param {Buffer} [seed] - Optional seed for key generation.
   * @returns {KeyPair} - The generated key pair containing both public and private keys.
   * @throws {Error} - If the private key is missing in the generated key pair.
   */
  public generateKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new IcpKeyPair({ seed }) : new IcpKeyPair();
    const { pub, prv } = keyPair.getKeys();
    if (!prv) {
      throw new Error('Private key is missing in the generated key pair.');
    }
    return { pub, prv };
  }

  /**
   * Validates the provided fee.
   *
   * @param {string} fee - The fee to validate.
   * @throws {BuildTransactionError} - If the fee is zero or invalid.
   */
  validateFee(fee: string): boolean {
    const feeValue = new BigNumber(fee);
    if (feeValue.isZero()) {
      throw new BuildTransactionError('Fee cannot be zero');
    }
    return true;
  }

  /** @inheritdoc */
  validateValue(value: BigNumber): boolean {
    if (value.isLessThanOrEqualTo(0)) {
      throw new BuildTransactionError('amount cannot be less than or equal to zero');
    }
    return true;
  }

  /**
   * Validates the provided memo.
   *
   * @param {number | BigInt} memo - The memo to validate.
   * @returns {boolean} - Returns `true` if the memo is valid.
   * @throws {BuildTransactionError} - If the memo is invalid.
   */
  validateMemo(memo: number | BigInt): boolean {
    const memoNumber = Number(memo);
    if (memoNumber < 0 || Number.isNaN(memoNumber)) {
      throw new BuildTransactionError('Invalid memo');
    }
    return true;
  }

  validateExpireTime(expireTime: number | BigInt): boolean {
    if (Number(expireTime) < Date.now() * 1000_000) {
      throw new BuildTransactionError('Invalid expiry time');
    }
    return true;
  }

  /**
   * Validates the raw transaction data to ensure it has a valid format in the blockchain context.
   *
   * @param {IcpTransactionData} transactionData - The transaction data to validate.
   * @throws {ParseTransactionError} If the transaction data is invalid.
   */
  validateRawTransaction(transactionData: IcpTransactionData): void {
    if (!transactionData) {
      throw new ParseTransactionError('Transaction data is missing.');
    }
    const { senderPublicKeyHex, senderAddress, receiverAddress } = transactionData;
    if (senderPublicKeyHex && !this.isValidPublicKey(senderPublicKeyHex)) {
      throw new ParseTransactionError('Sender public key is invalid.');
    }
    if (!this.isValidAddress(senderAddress)) {
      throw new ParseTransactionError('Sender address is invalid.');
    }
    if (!this.isValidAddress(receiverAddress)) {
      throw new ParseTransactionError('Receiver address is invalid.');
    }
    this.validateFee(transactionData.fee);
    this.validateValue(new BigNumber(transactionData.amount));
    this.validateMemo(transactionData.memo);
    this.validateExpireTime(transactionData.expiryTime);
  }

  /**
   *
   * @param {object} update
   * @returns {Buffer}
   */
  generateHttpCanisterUpdateId(update: HttpCanisterUpdate): Buffer {
    return this.HttpCanisterUpdateRepresentationIndependentHash(update);
  }

  /**
   * Generates a representation-independent hash for an HTTP canister update.
   *
   * @param {HttpCanisterUpdate} update - The HTTP canister update object.
   * @returns {Buffer} - The hash of the update object.
   */
  HttpCanisterUpdateRepresentationIndependentHash(update: HttpCanisterUpdate): Buffer {
    const updateMap = {
      request_type: RequestType.CALL,
      canister_id: update.canister_id,
      method_name: update.method_name,
      arg: update.arg,
      ingress_expiry: update.ingress_expiry,
      sender: update.sender,
    };
    return this.hashOfMap(updateMap);
  }

  /**
   * Generates a SHA-256 hash for a given map object.
   *
   * @param {Record<string, unknown>} map - The map object to hash.
   * @returns {Buffer} - The resulting hash as a Buffer.
   */
  hashOfMap(map: Record<string, any>): Buffer {
    const hashes: Buffer[] = [];
    for (const key in map) {
      hashes.push(this.hashKeyVal(key, map[key]));
    }
    hashes.sort((buf0, buf1) => buf0.compare(buf1));
    return this.sha256(hashes);
  }

  /**
   * Generates a hash for a key-value pair.
   *
   * @param {string} key - The key to hash.
   * @param {string | Buffer | BigInt} val - The value to hash.
   * @returns {Buffer} - The resulting hash as a Buffer.
   */
  hashKeyVal(key: string, val: any): Buffer {
    const keyHash = this.hashString(key);
    const valHash = this.hashVal(val);
    return Buffer.concat([keyHash, valHash]);
  }

  /**
   * Generates a SHA-256 hash for a given string.
   *
   * @param {string} value - The string to hash.
   * @returns {Buffer} - The resulting hash as a Buffer.
   */
  hashString(value: string): Buffer {
    return this.sha256([Buffer.from(value)]);
  }

  /**
   * Generates a hash for a 64-bit unsigned integer.
   *
   * @param {bigint} n - The 64-bit unsigned integer to hash.
   * @returns {Buffer} - The resulting hash as a Buffer.
   */
  hashU64(n: bigint): Buffer {
    const buf = Buffer.allocUnsafe(10);
    let i = 0;
    while (true) {
      const byte = Number(n & BigInt(0x7f));
      n >>= BigInt(7);
      if (n === BigInt(0)) {
        buf[i] = byte;
        break;
      } else {
        buf[i] = byte | 0x80;
        ++i;
      }
    }
    return this.hashBytes(buf.subarray(0, i + 1));
  }

  /**
   * Generates a SHA-256 hash for an array of elements.
   *
   * @param {Array<any>} elements - The array of elements to hash.
   * @returns {Buffer} - The resulting hash as a Buffer.
   */
  hashArray(elements: Array<any>): Buffer {
    return this.sha256(elements.map(this.hashVal));
  }

  /**
   * Generates a hash for a given value.
   *
   * @param {string | Buffer | BigInt | number | Array<unknown>} val - The value to hash.
   * @returns {Buffer} - The resulting hash as a Buffer.
   * @throws {Error} - If the value type is unsupported.
   */
  hashVal(val: string | Buffer | BigInt | number | Array<unknown>): Buffer {
    if (typeof val === 'string') {
      return utils.hashString(val);
    } else if (Buffer.isBuffer(val) || val instanceof Uint8Array) {
      return utils.hashBytes(val);
    } else if (typeof val === 'bigint' || typeof val === 'number') {
      return utils.hashU64(BigInt(val));
    } else if (Array.isArray(val)) {
      return utils.hashArray(val);
    } else {
      throw new Error(`Unsupported value type for hashing: ${typeof val}`);
    }
  }

  /**
   * Computes the SHA-256 hash of the given buffer.
   *
   * @param value - The buffer to hash.
   * @returns The SHA-256 hash of the input buffer.
   */
  hashBytes(value: Buffer | Uint8Array): Buffer {
    return this.sha256([value]);
  }

  /**
   * Computes the SHA-256 hash of the provided array of Buffer chunks.
   *
   * @param {Array<Buffer>} chunks - An array of Buffer objects to be hashed.
   * @returns {Buffer} - The resulting SHA-256 hash as a Buffer.
   */
  sha256(chunks: Array<Buffer> | Array<Uint8Array>): Buffer {
    const hasher = js_sha256.sha256.create();
    chunks.forEach((chunk) => hasher.update(chunk));
    return Buffer.from(hasher.arrayBuffer());
  }

  /**
   * Converts a hexadecimal string to a Buffer.
   *
   * @param hex - The hexadecimal string to convert.
   * @returns A Buffer containing the binary data represented by the hexadecimal string.
   */
  blobFromHex(hex: string): Buffer {
    return Buffer.from(hex, 'hex');
  }

  /**
   * Converts a binary blob (Buffer) to a hexadecimal string.
   *
   * @param {Buffer} blob - The binary data to be converted.
   * @returns {string} The hexadecimal representation of the binary data.
   */
  blobToHex(blob: Buffer): string {
    return blob.toString('hex');
  }

  /**
   * Decodes a given CBOR-encoded buffer.
   *
   * @param buffer - The CBOR-encoded buffer to decode.
   * @returns The decoded data.
   */
  cborDecode(buffer: Buffer): unknown {
    const res = decode(buffer);
    return res;
  }

  /**
   * Generates a Buffer containing the domain IC request string.
   *
   * @returns {Buffer} A Buffer object initialized with the string '\x0Aic-request'.
   */
  getDomainICRequest(): Buffer {
    return Buffer.from('\x0Aic-request');
  }

  /**
   * Combines the domain IC request buffer with the provided message ID buffer to create signature data.
   *
   * @param {Buffer} messageId - The buffer containing the message ID.
   * @returns {Buffer} - The concatenated buffer containing the domain IC request and the message ID.
   */
  makeSignatureData(messageId: Buffer): Buffer {
    return Buffer.concat([this.getDomainICRequest(), messageId]);
  }

  /**
   * Extracts the recipient information from the provided ICP transaction data.
   *
   * @param {IcpTransactionData} icpTransactionData - The ICP transaction data containing the receiver's address and amount.
   * @returns {Recipient[]} An array containing a single recipient object with the receiver's address and amount.
   */
  getRecipients(icpTransactionData: IcpTransactionData): Recipient {
    return {
      address: icpTransactionData.receiverAddress,
      amount: icpTransactionData.amount,
    };
  }

  getTransactionSignature(signatureMap: Map<string, Signatures>, update: HttpCanisterUpdate): Signatures | undefined {
    return signatureMap.get(this.blobToHex(this.makeSignatureData(this.generateHttpCanisterUpdateId(update))));
  }

  getMetaData(
    memo: number | BigInt,
    timestamp: number | bigint | undefined,
    ingressEnd: number | BigInt | undefined
  ): { metaData: MetaData; ingressEndTime: number | BigInt } {
    let currentTime = Date.now() * 1000000;
    if (timestamp) {
      currentTime = Number(timestamp);
    }
    let ingressStartTime: number, ingressEndTime: number;
    if (ingressEnd) {
      ingressEndTime = Number(ingressEnd);
      ingressStartTime = ingressEndTime - MAX_INGRESS_TTL; // 5 mins in nanoseconds
    } else {
      ingressStartTime = currentTime;
      ingressEndTime = ingressStartTime + MAX_INGRESS_TTL; // 5 mins in nanoseconds
    }
    const metaData: MetaData = {
      created_at_time: currentTime,
      ingress_start: ingressStartTime,
      ingress_end: ingressEndTime,
      memo: memo,
    };

    return { metaData, ingressEndTime };
  }

  convertSenderBlobToPrincipal(senderBlob: Uint8Array): Uint8Array {
    const MAX_LENGTH_IN_BYTES = 29;
    if (senderBlob.length > MAX_LENGTH_IN_BYTES) {
      throw new Error('Bytes too long for a valid Principal');
    }
    const principalBytes = new Uint8Array(MAX_LENGTH_IN_BYTES);
    principalBytes.set(senderBlob.slice(0, senderBlob.length));
    return principalBytes;
  }

  fromArgs(arg: Uint8Array): SendArgs {
    const SendRequestMessage = messageCompiled.SendRequest;
    const args = SendRequestMessage.decode(arg) as unknown as SendArgs;
    const transformedArgs: SendArgs = {
      payment: { receiverGets: { e8s: Number(args.payment.receiverGets.e8s) } },
      maxFee: { e8s: Number(args.maxFee.e8s) },
      to: { hash: Buffer.from(args.to.hash) },
      createdAtTime: { timestampNanos: BigNumber(args.createdAtTime.timestampNanos.toString()).toNumber() },
      memo: { memo: Number(args.memo.memo.toString()) },
    };
    return transformedArgs;
  }

  async toArg(args: SendArgs): Promise<Uint8Array> {
    const SendRequestMessage = messageCompiled.SendRequest;
    const errMsg = SendRequestMessage.verify(args);
    if (errMsg) throw new Error(errMsg);
    const message = SendRequestMessage.create(args as any);
    return SendRequestMessage.encode(message).finish();
  }

  getAccountIdPrefix(): Buffer<ArrayBuffer> {
    return Buffer.from([0x0a, ...Buffer.from('account-id')]);
  }

  /** @inheritdoc */
  isValidBlockId(hash: string): boolean {
    // ICP block hashes are 64-character hexadecimal strings
    return this.isValidHash(hash);
  }

  /**
   * Returns whether or not the string is a valid ICP hash
   *
   * @param {string} hash - string to validate
   * @returns {boolean}
   */
  isValidHash(hash: string): boolean {
    return typeof hash === 'string' && /^[0-9a-fA-F]{64}$/.test(hash);
  }

  /** @inheritdoc */
  isValidTransactionId(txId: string): boolean {
    return this.isValidHash(txId);
  }

  getSignatures(payloadsData: PayloadsData, senderPublicKey: string, senderPrivateKey: string): Signatures[] {
    return payloadsData.payloads.map((payload) => ({
      signing_payload: payload,
      signature_type: payload.signature_type,
      public_key: {
        hex_bytes: senderPublicKey,
        curve_type: CurveType.SECP256K1,
      },
      hex_bytes: this.signPayload(senderPrivateKey, payload.hex_bytes),
    }));
  }

  signPayload = (privateKey: string, payloadHex: string): string => {
    const privateKeyBytes = Buffer.from(privateKey, 'hex');
    const payloadHash = crypto.createHash('sha256').update(Buffer.from(payloadHex, 'hex')).digest('hex');
    const signature = secp256k1.sign(payloadHash, privateKeyBytes);
    const r = Buffer.from(signature.r.toString(16).padStart(64, '0'), 'hex');
    const s = Buffer.from(signature.s.toString(16).padStart(64, '0'), 'hex');
    return Buffer.concat([r, s]).toString('hex');
  };

  getTransactionId(unsignedTransaction: string, senderAddress: string, receiverAddress: string): string {
    try {
      const decodedTxn = utils.cborDecode(utils.blobFromHex(unsignedTransaction)) as CborUnsignedTransaction;
      const updates = decodedTxn.updates as unknown as [string, HttpCanisterUpdate][];
      for (const [, update] of updates) {
        const updateArgs = update.arg;
        const sendArgs = utils.fromArgs(updateArgs);
        const transactionHash = this.generateTransactionHash(sendArgs, senderAddress, receiverAddress);
        return transactionHash;
      }
      throw new Error('No updates found in the unsigned transaction.');
    } catch (error) {
      throw new Error(`Unable to compute transaction ID: ${error.message}`);
    }
  }

  safeBigInt(value: unknown): number | bigint {
    if (typeof value === 'bigint') {
      return value;
    }

    if (typeof value === 'number') {
      const isUnsafe = value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER;
      return isUnsafe ? BigInt(value) : value;
    }

    throw new Error(`Invalid type: expected a number or bigint, but received ${typeof value}`);
  }

  generateTransactionHash(sendArgs: SendArgs, senderAddress: string, receiverAddress: string): string {
    const senderAccount = this.accountIdentifier(senderAddress);
    const receiverAccount = this.accountIdentifier(receiverAddress);

    const transferFields = new Map<any, any>([
      [0, senderAccount],
      [1, receiverAccount],
      [2, new Map([[0, this.safeBigInt(Number(sendArgs.payment.receiverGets.e8s))]])],
      [3, new Map([[0, sendArgs.maxFee.e8s]])],
    ]);

    const operationMap = new Map([[2, transferFields]]);
    const txnFields = new Map<any, any>([
      [0, operationMap],
      [1, this.safeBigInt(sendArgs.memo.memo)],
      [2, new Map([[0, BigInt(sendArgs.createdAtTime.timestampNanos)]])],
    ]);

    const processedTxn = this.getProcessedTransactionMap(txnFields);
    const serializedTxn = encoder.encode(processedTxn);
    return crypto.createHash('sha256').update(serializedTxn).digest('hex');
  }

  accountIdentifier(accountAddress: string): AccountIdentifierHash {
    const bytes = Buffer.from(accountAddress, 'hex');
    if (bytes.length === 32) {
      return { hash: bytes.slice(4) };
    }
    throw new Error(`Invalid AccountIdentifier: 64 hex chars, got ${accountAddress.length}`);
  }

  getProcessedTransactionMap(txnMap: Map<any, any>): Map<any, any> {
    const operationMap = txnMap.get(0);
    const transferMap = operationMap.get(2);
    transferMap.set(0, this.serializeAccountIdentifier(transferMap.get(0)));
    transferMap.set(1, this.serializeAccountIdentifier(transferMap.get(1)));
    return txnMap;
  }

  serializeAccountIdentifier(accountHash: AccountIdentifierHash): string {
    if (accountHash && accountHash.hash) {
      const hashBuffer = accountHash.hash;
      const checksum = Buffer.alloc(4);
      checksum.writeUInt32BE(crc32.buf(hashBuffer) >>> 0, 0);
      return Buffer.concat([checksum, hashBuffer]).toString('hex').toLowerCase();
    }
    throw new Error('Invalid accountHash format');
  }
}

const utils = new Utils();
export default utils;

Выполнить команду


Для локальной разработки. Не используйте в интернете!